shop-applet/uni_modules/bt-cropper/components/bt-cropper/bt-cropper.vue

902 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="bt-container">
<!-- <view class="iconfont icon-reset" @click.stop="resetImage"></view> -->
<view @touchend="onTouchEnd" @touchstart="onTouchStart" class="mainContent">
<template v-if="imageInfo">
<image :src="showImagePath" mode="aspectFit" data-type="image" @touchmove.stop.prevent="onImageMove"
:style="[imageStyle]" class="image"></image>
<view class="cropper" :style="{
width: cropperPosition.width + 'px',
height: cropperPosition.height + 'px',
left: cropperPosition.left - 1 + 'px',
top: cropperPosition.top - 1 + 'px',
transition,
}">
<template v-if="showGrid">
<view class="line row row1"></view>
<view class="line row row2"></view>
<view class="line col col1"></view>
<view class="line col col2"></view>
</template>
</view>
<view @touchmove.stop.prevent="onHandleResize(-1, 0, $event)" class="controller vertical"
:style="[controllerPosition.left]" />
<view @touchmove.stop.prevent="onHandleResize(1, 0, $event)" class="controller vertical"
:style="[controllerPosition.right]" />
<view @touchmove.stop.prevent="onHandleResize(0, -1, $event)" class="controller horizon"
:style="[controllerPosition.top]" />
<view @touchmove.stop.prevent="onHandleResize(0, 1, $event)" class="controller horizon"
:style="[controllerPosition.bottom]" />
<template v-if="ratio==0">
<view @touchmove.stop.prevent="onHandleResize(-1, -1, $event)" class="controller controller_dot"
:style="[controllerPosition.leftTop]" />
<view @touchmove.stop.prevent="onHandleResize(1, -1, $event)" class="controller controller_dot"
:style="[controllerPosition.rightTop]" />
<view @touchmove.stop.prevent="onHandleResize(-1, 1, $event)" class="controller controller_dot"
:style="[controllerPosition.leftBottom]" />
<view @touchmove.stop.prevent="onHandleResize(1, 1, $event)" class="controller controller_dot"
:style="[controllerPosition.rightBottom]" />
</template>
</template>
</view>
<view class="slot">
<slot />
</view>
<!-- #ifdef MP-WEIXIN -->
<canvas v-if="showCanvas" type="2d" class="bt-canvas" :style="{
width:dSize.width+'px',
height:dSize.height+'px'
}"></canvas>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<canvas v-if="showCanvas" canvas-id="bt-canvas" class="bt-canvas" :style="{
width:dSize.width+'px',
height:dSize.height+'px'
}"></canvas>
<!-- #endif -->
</view>
</template>
<script>
/**
* better-cropper 图片裁切插件
*/
import calcImageSize from "./utils/calcImageSize.js"
import calcImagePosition from "./utils/calcImagePosition.js"
import calcCropper from "./utils/calcCropper.js"
import calcRightAndBottom from "./utils/calcRightAndBottom.js"
import calcPointDistance from "./utils/calcPointDistance.js"
import {
getTouchPoints,
sleep,
debounce,
log
} from "./utils/tools.js"
var startOffsetX = 0,
startOffsetY = 0,
startTouchsDistance = 0,
startChangeLeft = 0,
startChangeTop = 0,
startChangeWidth = 0,
startChangeHeight = 0,
initScale = 1,
startTouches = [],
timer = null;
export default {
name: "bt-cropper",
props: {
// 图片路径,支持网络路径和本地路径
imageSrc: {
type: String,
default: "",
required: true
// validator(value) {
// if (/^(http:|https:|)\/\//.test(value)) {
// return true
// } else {
// console.warn("图片url似乎不合法")
// return false
// }
// }
},
// 输出图片的格式默认jpg
fileType: {
type: String,
default: "jpg"
},
// 生成的图片的宽度
dWidth: {
type: Number,
default: 0
},
// 裁切比例0表示自由
ratio: {
type: Number,
default: 0,
validator(value) {
if (typeof value === 'number') {
if (value < 0) {
log('裁剪框比例值必须大于零', 'error')
return false
}
} else {
log('裁剪框比例值必须是数字', 'error')
return false
}
return true
}
},
// 是否展示网格
showGrid: {
type: Boolean,
default: false
},
// 图片质量0-1 越大质量越好
quality: {
type: Number,
default: 1
},
// 初始的图片位置
initPosition: {
type: Object,
default () {
return null
}
},
// 是否压缩图片
// 注意:这里的压缩指的是用户移动图片展示的那个图片的压缩,
// 输出图片的质量并不会受到影响,
// 也就是说,最终输出图片的质量会比裁切时看起来更好
compress: {
type: Boolean,
default: false
},
// 是否开启操作结束后自动放大
autoZoom: {
type: Boolean,
default: true
}
},
data() {
return {
imageUrl: "",
imageInfo: null,
containerRect: "",
offsetX: 0,
offsetY: 0,
changeWidth: 0,
changeHeight: 0,
windowWidth: 375,
dpr: 2,
forceChangeWidth: 0,
forceChangeHeight: 0,
animate: false,
imgScale: 1,
imgTranslateX: 0,
imgTranslateY: 0,
// 这个变量不要删掉不然会造成性能严重下降这是uni-app框架的Bug
showCanvas: false,
cropperPosition: {
left: 0,
top: 0,
width: 0,
height: 0
},
// 图片在视图上的位置
imageBoundingRect: {
left: 0,
top: 0,
width: 0,
height: 0,
}
};
},
watch: {
imageSrc: {
handler(imageSrc) {
this.imageUrl = imageSrc
this.imageInfo = null;
if (!imageSrc) {
return
}
this.getImageInfo(imageSrc).then(imageInfo => {
this.imageInfo = imageInfo;
return this.getContainerRect()
}).then(rect => {
this.containerRect = rect;
this.imageBoundingRect = this.getImageInitRect()
this.resetImage()
if (this.ratio == 0) {
this.cropperPosition = this.getCropperInitPosition()
}
return this.imageBoundingRect;
}).then((imageBoundingRect) => {
if (this.initPosition) {
const { left, top, width, height } = this.initPosition
const ratio = this.imageBoundingRect.width / this.imageInfo.width
if (left !== undefined) {
const imgTranslateX = this.cropperPosition.left - imageBoundingRect.left - left * ratio
this.cropperPosition.left -= imgTranslateX
}
if (top !== undefined) {
const imgTranslateY = this.cropperPosition.top - imageBoundingRect.top - top * ratio
this.cropperPosition.top -= imgTranslateY
}!!width && (this.cropperPosition.width = this.initPosition.width * ratio);
!!height && (this.cropperPosition.height = this.initPosition.height * ratio);
}
this.$emit('load')
}).catch(err => {
console.error('失败信息', err)
this.$emit('loadFail', err)
})
},
immediate: true
},
ratio: {
handler(ratio) {
this.resetCropper()
this.applyAnim()
},
immediate: false
},
},
computed: {
showImagePath() {
if (this.imageInfo && this.imageInfo.compressPath) {
return this.imageInfo.compressPath
} else {
return this.imageInfo?.path || ""
}
},
// 生成图片的大小
dSize() {
if (this.dWidth > 0) {
return {
width: this.dWidth,
height: this.dWidth * (this.cropperPosition.height / this.cropperPosition.width)
}
} else {
// 原像素比例裁剪
let scale = 1
if (this.imageInfo) {
scale = this.imgScale * this.imageBoundingRect.width / this.imageInfo.width
}
return {
width: this.cropperPosition.width / scale,
height: this.cropperPosition.height / scale
}
}
},
imageStyle() {
let style = {
left: this.imageBoundingRect.left + 'px',
top: this.imageBoundingRect.top + 'px',
width: this.imageBoundingRect.width + 'px',
height: this.imageBoundingRect.height + 'px',
transition: this.transition,
transform: `matrix(${this.imgScale}, 0, 0, ${this.imgScale}, ${this.imgTranslateX}, ${this.imgTranslateY}) translateZ(0px)`,
}
return style
},
transition() {
return this.animate ? '0.2s' : 'none'
},
// 四个控制点的位置
controllerPosition() {
const up40 = uni.upx2px(40),
up30 = uni.upx2px(30),
up20 = uni.upx2px(20);
const transition = this.transition
return {
left: {
left: this.cropperPosition.left - up30 + 'px',
top: this.cropperPosition.top + (this.cropperPosition.height) / 2 - up40 + 'px',
transition
},
right: {
left: this.cropperPosition.left + this.cropperPosition.width - up20 + 'px',
top: this.cropperPosition.top + (this.cropperPosition.height) / 2 - up40 + 'px',
transition
},
top: {
left: this.cropperPosition.left + this.cropperPosition.width / 2 - up40 + 'px',
top: this.cropperPosition.top - up30 + 'px',
transition
},
bottom: {
left: this.cropperPosition.left + this.cropperPosition.width / 2 - up40 + 'px',
top: this.cropperPosition.top + this.cropperPosition.height - up20 + 'px',
transition
},
leftTop: {
left: this.cropperPosition.left - up40 + 'px',
top: this.cropperPosition.top - up40 + 'px',
transition
},
rightTop: {
left: this.cropperPosition.left + this.cropperPosition.width - up40 + 'px',
top: this.cropperPosition.top - up40 + 'px',
transition
},
leftBottom: {
left: this.cropperPosition.left - up40 + 'px',
top: this.cropperPosition.top + this.cropperPosition.height - up40 + 'px',
transition
},
rightBottom: {
left: this.cropperPosition.left + this.cropperPosition.width - up40 + 'px',
top: this.cropperPosition.top + this.cropperPosition.height - up40 + 'px',
transition
}
}
},
},
methods: {
// 开启动画
applyAnim() {
this.animate = true
clearTimeout(timer)
timer = setTimeout(() => {
this.animate = false
}, 200)
},
async getContainerRect() {
const systemInfo = uni.getSystemInfoSync()
this.windowWidth = systemInfo.windowWidth
this.dpr = systemInfo.pixelRatio
return new Promise(resolve => {
uni.createSelectorQuery().in(this).select(".mainContent").boundingClientRect((rect) => {
resolve(rect)
}).exec()
})
},
async getImageInfo(imageSrc) {
uni.showLoading({
title: "获取图片信息"
})
return uni.getImageInfo({
src: imageSrc
}).then((res) => {
let imageInfo = null;
// #ifdef VUE2
const err = res[0];
imageInfo = res[1];
if (err) {
throw new Error(err)
}
// #endif
// #ifdef VUE3
imageInfo = res
// #endif
uni.hideLoading()
return imageInfo;
}).then(imageInfo => {
const maxSize = 2000 * 2000
const imageSize = imageInfo.width * imageInfo.height
// uni.canIUse('compressImage.success.tempFilePath')
// #ifndef H5
// 是否压缩图片
if (maxSize < imageSize && this.compress) {
const quality = maxSize / imageSize * 100
return uni.compressImage({
src: imageInfo.path,
quality
}).then(([err, {
tempFilePath
}]) => {
if (!err) {
imageInfo.compressPath = tempFilePath
}
return imageInfo
})
}
// #endif
return imageInfo;
}).then(imageInfo => {
return imageInfo;
}).catch(err => {
uni.hideLoading()
uni.showToast({
title: "图片加载失败",
icon: "none"
})
throw Error(err)
})
},
getImageInitRect() {
return calcImageSize(this.imageInfo, this.containerRect)
},
resetImage() {
this.imgTranslateX = 0
this.imgTranslateY = 0
this.imgScale = 1
this.resetCropper()
},
// 获取裁剪框初始位置
getCropperInitPosition() {
return calcCropper(this.imageBoundingRect, {
width: this.ratio || 1,
height: 1
})
},
/**
* @deprecated 本方法已弃用请用resetCropper代替
*/
resetRatio() {
log('本方法(resetRatio)已弃用,未来将删除,请用resetCropper代替', 'warn')
return this.resetCropper()
},
// 重置裁剪框
resetCropper() {
if (this.imageInfo && this.ratio !== 0) {
this.cropperPosition = this.getCropperInitPosition()
this.checkImagePosition()
}
},
onTouchStart(ev) {
this.animate = false
if (timer) {
clearTimeout(timer)
}
startTouches = Array.from(ev.touches)
if (ev.target.dataset.type === 'image') {
startOffsetX = this.imgTranslateX;
startOffsetY = this.imgTranslateY;
if (ev.touches.length == 2) {
initScale = this.imgScale
startTouchsDistance = calcPointDistance(...getTouchPoints(startTouches))
}
} else {
startChangeLeft = this.cropperPosition.left
startChangeTop = this.cropperPosition.top
startChangeWidth = this.cropperPosition.width;
startChangeHeight = this.cropperPosition.height;
}
},
onTouchEnd() {
startTouches = []
this.checkImagePosition()
if (this.autoZoom) {
clearTimeout(timer)
timer = setTimeout(this.zoom, 1000)
}
this.reportChange()
},
getImagePosition() {
return calcImagePosition(this.imageBoundingRect, {
imgTranslateX: this.imgTranslateX,
imgTranslateY: this.imgTranslateY,
imgScale: this.imgScale
})
},
getCropperPosition() {
return calcRightAndBottom(this.cropperPosition)
},
// 检查图片位置
checkImagePosition() {
const imagePosition = this.getImagePosition()
const cropperPosition = this.getCropperPosition()
// 如果裁剪框大于图像大小,就放大到裁剪框大小
const widthScale = cropperPosition.width / imagePosition.width
const heightScale = cropperPosition.height / imagePosition.height
const scale = Math.max(widthScale, heightScale)
if (scale > 1) {
this.imageZoom({
left: cropperPosition.left + cropperPosition.width / 2,
top: cropperPosition.top + cropperPosition.height / 2
}, scale)
this.applyAnim()
}
// 判断是否超出边界
if (imagePosition.left > cropperPosition.left) {
this.imgTranslateX = this.imgTranslateX - (imagePosition.left - cropperPosition.left)
} else if (imagePosition.right < cropperPosition.right) {
this.imgTranslateX = this.imgTranslateX + (cropperPosition.right - imagePosition.right)
}
if (imagePosition.top > cropperPosition.top) {
this.imgTranslateY = this.imgTranslateY - (imagePosition.top - cropperPosition.top)
} else if (imagePosition.bottom < cropperPosition.bottom) {
this.imgTranslateY = this.imgTranslateY + (cropperPosition.bottom - imagePosition.bottom)
}
},
zoom() {
// 容器比例
const containerRatio = this.containerRect.width / this.containerRect.height
// 移动后的裁剪框比例
const cropperRatio = this.cropperPosition.width / this.cropperPosition.height
// 放大比例
let scale = 1
if (cropperRatio > containerRatio) {
scale = this.containerRect.width / this.cropperPosition.width
} else {
scale = this.containerRect.height / this.cropperPosition.height
}
// 放大裁剪框
this.cropperPosition.width *= scale
this.cropperPosition.height *= scale
// // 移动图像
this.imageZoom({
left: this.cropperPosition.left,
top: this.cropperPosition.top
}, scale)
// 将裁剪框上下居中
const cropperTop = (this.containerRect.height - this.cropperPosition.height) / 2
// 需要上下移动的距离
const moveTop = cropperTop - this.cropperPosition.top
// 将裁剪框左右居中
const cropperLeft = (this.containerRect.width - this.cropperPosition.width) / 2
// 需要左右移动的距离
const moveLeft = (cropperLeft - this.cropperPosition.left)
this.cropperPosition.left = cropperLeft
this.cropperPosition.top = cropperTop
// 移动图像使之与裁剪框对齐
this.imgTranslateX += moveLeft
this.imgTranslateY += moveTop
this.checkImagePosition()
this.applyAnim()
},
// 发送change事件
reportChange() {
const imagePosition = this.getImagePosition()
const cropperPosition = this.getCropperPosition()
const scale = this.imageBoundingRect.width / this.imageInfo.width * this.imgScale
this.$emit('change', {
left: (cropperPosition.left - imagePosition.left) / scale,
top: (cropperPosition.top - imagePosition.top) / scale,
width: cropperPosition.width / scale,
height: cropperPosition.height / scale
})
},
imageZoom(center = {
left: 0,
top: 0
}, scale = 1) {
const imagePosition = this.getImagePosition()
this.imgScale = this.imgScale * scale
const offsetLeftPercent = (center.left - imagePosition.left) / imagePosition.width
this.imgTranslateX = this.imgTranslateX + imagePosition.width * (scale - 1) / 2 * (1 - offsetLeftPercent *
2)
const offsetTopPercent = (center.top - imagePosition.top) / imagePosition.height
this.imgTranslateY = this.imgTranslateY + imagePosition.height * (scale - 1) / 2 * (1 - offsetTopPercent *
2)
},
onImageMove(ev) {
if (ev.touches.length == 2 && startTouches.length == 2) {
const points = getTouchPoints(ev.touches)
const imgScale = initScale * calcPointDistance(...points) / startTouchsDistance
this.imageZoom({
left: this.cropperPosition.left + this.cropperPosition.width / 2,
top: this.cropperPosition.top + this.cropperPosition.height / 2
}, imgScale / this.imgScale)
} else if (ev.touches.length == 1 && startTouches.length == 1) {
const [startClientX, startClientY] = getTouchPoints(startTouches)[0]
const [clientX, clientY] = getTouchPoints(ev.touches)[0]
this.imgTranslateX = startOffsetX + clientX - startClientX
this.imgTranslateY = startOffsetY + clientY - startClientY
}
},
// 调整裁剪框大小
onHandleResize(pX, pY, ev) {
const [startClientX, startClientY] = getTouchPoints(startTouches)[0]
const [clientX, clientY] = getTouchPoints(ev.touches)[0]
const cropperBoundingRect = this.getCropperPosition()
const imageBoundingRect = this.getImagePosition()
const changeX = clientX - startClientX
const changeY = clientY - startClientY
const minSize = {
width: uni.upx2px(100),
height: uni.upx2px(100)
}
const imageRemainHeight = imageBoundingRect.bottom - cropperBoundingRect.top
const cropperRemainHeight = this.containerRect.bottom - cropperBoundingRect.top
const maxHeight = Math.min(imageRemainHeight, cropperRemainHeight)
const imageRemainWidth = imageBoundingRect.right - cropperBoundingRect.left
const cropperRemainWidth = this.containerRect.right - cropperBoundingRect.left
const maxWidth = Math.min(imageRemainWidth, cropperRemainWidth)
let width = 0
switch (pX) {
case 1:
width = startChangeWidth + changeX
if (width < maxWidth) {
if (width > minSize.width) {
this.cropperPosition.width = width
}
}
break;
case -1:
const left = startChangeLeft + changeX
const minLeft = Math.min(imageBoundingRect.left, cropperBoundingRect.left)
width = startChangeWidth - changeX
if (left > minLeft) {
if (width > minSize.width) {
this.cropperPosition.left = left
this.cropperPosition.width = width
}
}
break;
case 0:
if (this.ratio != 0)
this.cropperPosition.width = this.cropperPosition.height * this.ratio
break
}
switch (pY) {
case 1:
const height = startChangeHeight + changeY
if (height < maxHeight && height > minSize.height) {
this.cropperPosition.height = height
}
break;
case -1:
const top = startChangeTop + changeY
const minTop = Math.min(imageBoundingRect.top, cropperBoundingRect.top)
if (top > minTop) {
const height = startChangeHeight - changeY
if (height > minSize.height) {
this.cropperPosition.top = top
this.cropperPosition.height = height
}
}
break;
case 0:
if (this.ratio != 0)
this.cropperPosition.height = this.cropperPosition.width / this.ratio;
break;
}
},
// 开始裁剪
async crop(images = null) {
// 批量裁剪,暂时还没实现
// if (Array.isArray(images) && images.length) {
// for (item in images) {
// }
// return;
// }
if (!this.imageInfo) {
uni.showToast({
title: "图片尚未载入完成"
})
return [new Error("图片尚未载入完成"), null]
}
this.showCanvas = true
this.$emit('cropStart')
return new Promise(resolve => {
this.$nextTick(() => {
this.onCrop().then(res => {
resolve(res)
})
})
})
},
// 开始裁切
async onCrop() {
let canvas, image, ctx, err, res;
// 新版canvas
// #ifdef MP-WEIXIN
canvas = await new Promise((resolve) => {
uni
.createSelectorQuery().in(this)
.select(".bt-canvas")
.node((res) => {
resolve(res.node);
})
.exec();
});
log("在小程序模拟器上可能会裁剪失败,真机无此问题,放心使用", "warn")
image = canvas.createImage();
image.src = this.imageInfo.path;
await new Promise((resolve) => (image.onload = resolve));
canvas.width = this.dSize.width;
canvas.height = this.dSize.height;
ctx = canvas.getContext("2d");
// #endif
// #ifndef MP-WEIXIN
image = this.imageInfo.path
ctx = uni.createCanvasContext("bt-canvas", this)
// #endif
const imagePosition = this.getImagePosition()
const cropperPosition = this.getCropperPosition()
const scale = imagePosition.width / this.imageInfo.width
const offsetLeft = (cropperPosition.left - imagePosition.left) / scale
const offsetTop = (cropperPosition.top - imagePosition.top) / scale
const cropperWidth = cropperPosition.width / scale
const cropperHeight = cropperPosition.height / scale
// console.log('offsetLeft', offsetLeft, 'offsetTop', offsetTop, this.imageInfo)
ctx.drawImage(
image,
offsetLeft,
offsetTop,
cropperWidth,
cropperHeight,
0,
0,
this.dSize.width,
this.dSize.height
);
// #ifndef MP-WEIXIN
await new Promise((resolve) => ctx.draw(true, resolve));
// #endif
// 等待一段时间不然ios会裁剪失败
await sleep(200);
// 在vue3里面只能写成这种回调形式否则报错
[err, res] = await new Promise(resolve => {
uni.canvasToTempFilePath({
// #ifdef MP-WEIXIN
canvas,
// #endif
// #ifndef MP-WEIXIN
canvasId: "bt-canvas",
// #endif
fileType: this.fileType,
destWidth: this.dSize.width,
destHeight: this.dSize.height,
quality: this.quality,
success(res) {
// console.log("裁剪成功")
resolve([null, res])
},
fail(err) {
// console.log("裁剪失败", err)
resolve([err, null])
},
complete: () => {
this.showCanvas = false
}
});
})
this.$emit('cropEnd', [err, res])
return [err, res]
},
},
};
</script>
<style lang="scss" scoped>
@import "./iconfont.css";
.bt-container {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
box-sizing: border-box;
background-color: #0e1319;
padding-top: 30rpx;
position: relative;
overflow: hidden;
.iconfont {
position: absolute;
z-index: 999;
top: 40rpx;
font-size: 30rpx;
padding: 10rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 50%;
color: #FFFFFF;
&.active {
color: #007AFF;
}
}
.icon-move {
right: 100rpx;
}
.icon-reset {
right: 40rpx;
}
.bt-canvas {
position: absolute;
left: 100%;
top: 0;
width: 300px;
height: 300px;
}
.mainContent {
flex: 1;
margin: 60rpx 60rpx 150rpx;
position: relative;
.image {
position: absolute;
will-change: transform;
transform-origin: center center;
}
.controller {
position: absolute;
z-index: 99;
padding: 20rpx;
&::after {
display: block;
content: '';
box-shadow: 0 0 10rpx #333;
background-color: #E4E7ED;
}
&.controller_dot {
&::after {
width: 40rpx;
height: 40rpx;
border-radius: 99px;
}
}
&.vertical {
&::after {
width: 10rpx;
height: 40rpx;
}
}
&.horizon {
&::after {
width: 40rpx;
height: 10rpx;
}
}
}
.cropper {
position: absolute;
border: 1px solid #eee;
box-sizing: content-box;
transform-origin: center center;
outline: 999px solid rgba(0, 0, 0, 0.5);
will-change: transform;
display: contain;
pointer-events: none;
.line {
position: absolute;
// background-color: #eee;
}
.row {
width: 100%;
height: 0px;
left: 0;
border-top: 1px dashed #007AFF;
}
.col {
height: 100%;
width: 0px;
border-left: 1px dashed #007AFF;
}
.row1 {
top: 33%;
}
.row2 {
top: 66%;
}
.col1 {
left: 33%;
}
.col2 {
left: 66%;
}
}
}
.slot {
position: fixed;
width: 100%;
left: 0;
bottom: 131.58rpx;
height: 90rpx;
height: calc(90rpx+ constant(safe-area-inset-bottom)); ///兼容 IOS<11.2/
height: calc(90rpx + env(safe-area-inset-bottom)); ///兼容 IOS>11.2/
}
}
</style>