记录---前端实现签字效果+合同展示
🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
获取一个高度会变的元素的高度
这里获取高度只是为了用遮罩层覆盖住可编辑的内容!
script 代码:
1 2 3 4 5 6 7 8 9 | let bigBoxHeight = ref(0); // 获取到元素 let bigBox = document.querySelector( ".bigBox" ); // 设置高度为 auto bigBox.style.height = "auto" ; // 获取 offsetHeight const height = bigBox.offsetHeight; // 设置值 bigBoxHeight.value = height; |
注:
offsetHeight:返回一个元素的高度,包括其padding和border,但不包括其margin。
template 代码:
1 2 3 4 5 6 7 | <div class= "bigBox" :style= "{ height: bigBoxHeight + 'px' }" > <div class= "contractBox" > <div v -html= "printData" >< /div > < /div > <!-- 遮罩层,返回的printData里设置了可编辑,但是这里只是展示用,且修改了也不会有影响,所以就简单的加个遮罩就行了 --> <div class= "markBox" :style= "{ height: bigBoxHeight + 'px' }" >< /div > < /div > |
获取元素设置的 transform
感谢:原生js获取元素transform的scale和rotate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 获取设置了transform的元素 let contractBox = document.querySelector( ".contractBox" ); // 获取浏览器计算后的结果 let st = window.getComputedStyle(contractBox, null); // 从结算后的结果中找到 transform,也可以直接 st.transform var tr = st.getPropertyValue( "transform" ); if ( tr === "none" ) { // 为none表示未设置 bigBox.style.height = "auto" ; const height = bigBox.offsetHeight + 50; bigBoxHeight.value = height; } else { bigBox.style.height = "auto" ; // 缩放需要 * 缩放比例 + 边距(margin /padding ) const height = bigBox.offsetHeight * 0.5 + 50; bigBoxHeight.value = height; } |
适配手机
上面设置 transform 是因为返回的 html 文档不是自适应的,所以菜鸟就在手机端,让其渲染700px,但是再缩小0.5倍去展示,即可解决!
css 代码:
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 | @media screen and (max-width: 690px) { .contractBox { width: 700px !important; transform: scale(0.5); // 防止按中心点缩放而导致上面留白很多(合同很长,7000px左右) transform-origin: 5% 0; } } .bigBox { position: relative; // 设置是因为 scale 缩放了但是元素还是占本身那么大,所以要超出隐藏 overflow: hidden; .markBox { width: 100%; position: absolute; left: 0; bottom: 0; top : 0; bottom: 0; } } .contractBox { width: 70%; margin: 50px auto 0px; overflow: hidden; } |
transform-origin: 5% 0; 的原因
这里设置 5% 是为了居中,因为这里有个问题就是不能设置 bigBox 为 display:flex,不然里面的内容就是按照width:100% 然后缩放0.5,而不是width:700px来缩放的!
是flex搞的鬼,菜鸟这里就用了个简单办法。
其实正统做法应该是获取宽度,再用窗口宽度减去获取的宽度 / 2,然后通过该值设置margin!
修改后
菜鸟既然想到了上面的居中方式,那就直接实现了,这里给上代码!
script 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 是否缩放,来确定margin-left取值 let isScale = ref( false ); let bigBoxmargin = ref(0); let bigBox = document.querySelector( ".bigBox" ); let contractBox = document.querySelector( ".contractBox" ); let st = window.getComputedStyle(contractBox, null); var tr = st.getPropertyValue( "transform" ); if ( tr === "none" ) { isScale.value = false ; bigBox.style.height = "auto" ; const height = bigBox.offsetHeight + 50; bigBoxHeight.value = height; } else { isScale.value = true ; bigBox.style.height = "auto" ; // 缩放需要 * 缩放比例 + 边距(margin /padding ) const height = bigBox.offsetHeight * 0.5 + 50; // 不用 st.witdh 是因为 st.witdh 获取的值是 700px,不能直接运算,这里菜鸟就偷懒了,不想处理了 bigBoxmargin.value = (window.innerWidth - 700 * 0.5) / 2; bigBoxHeight.value = height; } |
template 代码
1 2 3 4 5 6 | <div class= "bigBox" :style= "{ height: bigBoxHeight + 'px' }" > <div class= "contractBox" :style= "{ marginLeft: isScale ? bigBoxmargin + 'px' : 'auto' }" > <div v -html= "printData" >< /div > < /div > <div class= "markBox" :style= "{ height: bigBoxHeight + 'px' }" >< /div > < /div > |
签字效果
这里签字效果,菜鸟是使用 el-dialog 实现的,el-dialog 的使用方式见:element plus 使用细节
这里主要粘贴签字的代码
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | <script setup> import { ref, onMounted, nextTick } from "vue" ; const props = defineProps({ dialogVisible: { type : Boolean, default: false , }, }); const emit = defineEmits([ "closeEvent" ]); // 关闭弹窗 function handleClose() { emit( "closeEvent" , false ); // 解除禁止页面滚动 document.body.removeEventListener( "touchmove" , preventDefault); } const dialogBox = ref(); function closeDialog() { dialogBox.value.resetFields(); } // 禁止页面滚动 function preventDefault(e) { e.preventDefault(); } document.body.addEventListener( "touchmove" , preventDefault, { passive: false }); // 签名 // 配置内容 const config = { width: window.innerWidth, // 宽度 height: window.innerHeight - 300, // 高度,减300是为了给dialog的footer一点空间显示 lineWidth: 5, // 线宽 strokeStyle: "red" , // 线条颜色 lineCap: "round" , // 设置线条两端圆角 lineJoin: "round" , // 线条交汇处圆角 }; let canvas = null; let ctx = null; onMounted(async () => { await nextTick(); // 获取canvas 实例 canvas = document.querySelector( ".canvas" ); // 设置宽高 canvas.width = config.width; canvas.height = config.height; // 设置一个边框 canvas.style.border = "1px solid #000" ; // 创建上下文 ctx = canvas.getContext( "2d" ); // 设置填充背景色 ctx.fillStyle = "transparent" ; // 绘制填充矩形 ctx.fillRect( 0, // x 轴起始绘制位置 0, // y 轴起始绘制位置 config.width, // 宽度 config.height // 高度 ); }); // 保存上次绘制的 坐标及偏移量 const client = { offsetX: 0, // 偏移量 offsetY: 0, endX: 0, // 坐标 endY: 0, }; // 判断是否为移动端 const mobileStatus = /Mobile |Android|iPhone /i . test (navigator.userAgent); // 初始化 const init = (event) => { // 获取偏移量及坐标 const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event; // 修改上次的偏移量及坐标 client.offsetX = offsetX; client.offsetY = offsetY; client.endX = pageX; client.endY = pageY; // 清除以上一次 beginPath 之后的所有路径,进行绘制 ctx.beginPath(); // 根据配置文件设置相应配置 ctx.lineWidth = config.lineWidth; ctx.strokeStyle = config.strokeStyle; ctx.lineCap = config.lineCap; ctx.lineJoin = config.lineJoin; // 设置画线起始点位 ctx.moveTo(client.endX, client.endY); // 监听 鼠标移动或手势移动 window.addEventListener(mobileStatus ? "touchmove" : "mousemove" , draw); }; // 绘制 const draw = (event) => { console.log(event); // 获取当前坐标点位 const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event; // 超出范围不监听 if (pageY > config.height) { return ; } // 修改最后一次绘制的坐标点 client.endX = pageX; client.endY = pageY; // 根据坐标点位移动添加线条 ctx.lineTo(pageX, pageY); // 绘制 ctx.stroke(); }; // 结束绘制 const cloaseDraw = () => { // 结束绘制 ctx.closePath(); // 移除鼠标移动或手势移动监听器 window.removeEventListener( "mousemove" , draw); }; // 创建鼠标/手势按下监听器 window.addEventListener(mobileStatus ? "touchstart" : "mousedown" , init); // 创建鼠标/手势 弹起/离开 监听器 window.addEventListener(mobileStatus ? "touchend" : "mouseup" , cloaseDraw); // 取消-清空画布 const cancel = () => { // 清空当前画布上的所有绘制内容 ctx.clearRect(0, 0, config.width, config.height); }; // 保存-将画布内容保存为图片 const save = () => { // 将canvas上的内容转成blob流 canvas.toBlob((blob) => { // 获取当前时间并转成字符串,用来当做文件名 const date = Date.now().toString(); // 创建一个 a 标签 const a = document.createElement( "a" ); // 设置 a 标签的下载文件名 a.download = `${ date }.png`; // 设置 a 标签的跳转路径为 文件流地址 a.href = URL.createObjectURL(blob); // 手动触发 a 标签的点击事件 a.click(); // 移除 a 标签 a.remove(); }); handleClose(); }; < /script > <template> <div> <el-dialog title= "签字" ref= "dialogBox" :modelValue= "dialogVisible" :before-close= "handleClose" @close= "closeDialog" :close-on-click-modal= "false" :destroy-on-close= "true" top = "0" width= "100%" > <canvas class= "canvas" >< /canvas > <template #footer> <div> <el-button type = "primary" @click= "save" >保存< /el-button > <el-button @click= "cancel" >清除< /el-button > <el-button @click= "handleClose" >关闭< /el-button > < /div > < /template > < /el-dialog > < /div > < /template > <style lang= "scss" > .el-dialog__header { display: none; } .el-dialog__body { padding: 0 !important; } < /style > |
取消el-dialog的头部+边距
因为这里的 client 设置的偏移量都是 0,菜鸟不会改(感觉应该加上el-dialog的头部+边框的偏移量),如果不取消的话,就是错位着写的!
为什么禁止界面滚动
这里禁止是因为手机端,签名时写 “竖” 操作时,容易触发下拉整个界面的事件!导致写字中断,体验感极差,所以弹窗弹出时阻止事件,关闭后移除!
这里函数 preventDefault 必须提出,不然会取消不掉!
vue3 使用 nextTick
获取元素必须在 onMounted 中,但是 el-dialog 即使写在 onMounted 里面也不行,需要加上 nextTick !
实现效果
签字判断是横是竖
今天菜鸟又遇见了大麻烦,就是这个签字不能知道别人是横屏横着写的还是竖屏横着写的,eg:
菜鸟的思路就是获取到签字部分,然后如果横着签字就直接截取那部分设置样式,如果竖着签字就设置样式旋转 -90deg,那如何获取签字部分的大小呢?
canvas 去掉空白部分
canvas 去掉空白部分
修改上面 save 中代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const save = () => { // 将canvas上的内容转成blob流 var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; var lOffset = canvas.width, rOffset = 0, tOffset = canvas.height, bOffset = 0; for (var i = 0; i < canvas.width; i++) { for (var j = 0; j < canvas.height; j++) { var pos = (i + canvas.width * j) * 4; if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) { // 说第j行第i列的像素不是透明的 // 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断 bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标 rOffset = Math.max(i, rOffset); // 找到有色彩的最右端 tOffset = Math.min(j, tOffset); // 找到有色彩的最上端 lOffset = Math.min(i, lOffset); // 找到有色彩的最左端 } } } } |
canvas.getContext('2d').getImageData(0, 0, 宽, 高) 会返回一个当前 canvas 的图像数据对象,其中有一个data属性,是一个一维数组,这个一维数组,每4个下标分别代表了一个像素点的 R,G,B,A 的值,只需要遍历这些值就能找到边界了。
更多canvas 常用操作可以见:渡一学习笔记:canvas、css滤镜、特效、svg
canvas裁剪图片
canvas裁剪图片
但是获取到了区域并不行,因为我还需要将其截取,然后转成图片传给后端,且还要让后端知道到底是横着放 html 模板里还是竖着放,思来想去,感觉直接返回 base64 的 img 元素给后端更好,因为我就可以直接设置style,后端只需要放到对应的地方就行,所以save继续修改为
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 | const save = () => { // 将canvas上的内容转成blob流 var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; var lOffset = canvas.width, rOffset = 0, tOffset = canvas.height, bOffset = 0; for (var i = 0; i < canvas.width; i++) { for (var j = 0; j < canvas.height; j++) { var pos = (i + canvas.width * j) * 4; if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) { // 说第j行第i列的像素不是透明的 // 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断 bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标 rOffset = Math.max(i, rOffset); // 找到有色彩的最右端 tOffset = Math.min(j, tOffset); // 找到有色彩的最上端 lOffset = Math.min(i, lOffset); // 找到有色彩的最左端 } } } // 重新创建一个canvas,将之前的canvas上的图片,按照获取到的大小去截取 const trimmedWidth = rOffset - lOffset + 1; const trimmedHeight = bOffset - tOffset + 1; const trimmedCanvas = document.createElement( "canvas" ); trimmedCanvas.width = trimmedWidth; trimmedCanvas.height = trimmedHeight; const trimmedContext = trimmedCanvas.getContext( "2d" ); trimmedContext.putImageData( ctx.getImageData(lOffset, tOffset, trimmedWidth, trimmedHeight), 0, 0 ); // 将截取后的生成图片,并设置样式 console.log(trimmedWidth); console.log(trimmedHeight); var newUrl = trimmedCanvas.toDataURL(); var newImage = new Image(); newImage.src = newUrl; console.log(trimmedWidth < trimmedHeight); if (trimmedWidth < trimmedHeight) { newImage.style.height = "100px" ; newImage.style.transform = "rotate(-" + 90 + "deg)" ; } else { newImage.style.width = "100px" ; } console.log(newImage); handleClose(); }; |
newImage 打印出来是一个元素:
1 | <img src= "data:image/png;base64,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" style= "transform: rotate(-90deg);" > |
至此算是完成了整个签字功能!
最终完善代码,可以直接使用
其实这里还有一个问题,就是不知道横屏后的用户到底是哪边横屏,可能要旋转-90deg,也可能是正90deg,这里菜鸟想的是一个简单办法,就是给个示例文字,让用户根据示例文字进行签字!
template
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 75 76 | <template> <div> <el-dialog title= "签字" ref= "dialogBox" :modelValue= "dialogVisible" :before-close= "handleClose" @close= "closeDialog" :close-on-click-modal= "false" :destroy-on-close= "true" top = "0" width= "100%" > <div class= "canvasBox" :style= "{ height: config.height + 'px' }" > <canvas class= "canvas" >< /canvas > <div class= "tipTxt" v -show= "showTip" > <p>示例文字!< /p > <p class= "redTxt" >请按示例文字方向,正楷清晰书写。谢谢!< /p > < /div > < /div > <template #footer> <div> <el-button type = "primary" @click= "save" >保存< /el-button > <el-button @click= "cancel" >清除< /el-button > <el-button @click= "handleClose(false)" >关闭< /el-button > < /div > < /template > < /el-dialog > < /div > < /template > <style lang= "scss" > .el-dialog__header { display: none; } .el-dialog__body { padding: 0 !important; } .canvasBox { position: relative; } .canvas, .tipTxt { position: absolute; top : 0; left: 0; bottom: 0; right: 0; } .tipTxt { display: flex; flex-direction: column; align-items: center; justify-content: center; } .tipTxt p { color: #999; font-size: 80px; } .tipTxt .redTxt { color: rgba(255, 0, 0, 0.5); font-size: 50px; } @media screen and (max-width: 690px) { .tipTxt { transform: rotate(90deg); } .tipTxt p { font-size: 50px; } .tipTxt .redTxt { font-size: 18px; } } < /style > |
js 控制 showTip 的展示
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | <script setup> import { ref, onMounted, nextTick } from "vue" ; // eslint-disable-next-line const props = defineProps({ dialogVisible: { type : Boolean, default: false , }, }); // eslint-disable-next-line const emit = defineEmits([ "closeEvent" ]); // 关闭弹窗 function handleClose(imgEl) { console.log(imgEl); emit( "closeEvent" , imgEl); // 禁止页面滚动 document.body.removeEventListener( "touchmove" , preventDefault); } const dialogBox = ref(); function closeDialog() { dialogBox.value.resetFields(); } // 禁止页面滚动 function preventDefault(e) { e.preventDefault(); } document.body.addEventListener( "touchmove" , preventDefault, { passive: false }); // 签名 // 配置内容 const config = { width: window.innerWidth, // 宽度 height: window.innerHeight - 150, // 高度 lineWidth: 5, // 线宽 strokeStyle: "red" , // 线条颜色 lineCap: "round" , // 设置线条两端圆角 lineJoin: "round" , // 线条交汇处圆角 }; let canvas = null; let ctx = null; onMounted(async () => { await nextTick(); // 获取canvas 实例 canvas = document.querySelector( ".canvas" ); console.log(canvas); // 设置宽高 canvas.width = config.width; canvas.height = config.height; // 设置一个边框 canvas.style.border = "1px solid #000" ; // 创建上下文 ctx = canvas.getContext( "2d" ); // 设置填充背景色 ctx.fillStyle = "transparent" ; // 绘制填充矩形 ctx.fillRect( 0, // x 轴起始绘制位置 0, // y 轴起始绘制位置 config.width, // 宽度 config.height // 高度 ); }); // 保存上次绘制的 坐标及偏移量 const client = { offsetX: 0, // 偏移量 offsetY: 0, endX: 0, // 坐标 endY: 0, }; // 判断是否为移动端 const mobileStatus = /Mobile |Android|iPhone /i . test (navigator.userAgent); // 初始化 const init = (event) => { // 获取偏移量及坐标 const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event; // 修改上次的偏移量及坐标 client.offsetX = offsetX; client.offsetY = offsetY; client.endX = pageX; client.endY = pageY; // 清除以上一次 beginPath 之后的所有路径,进行绘制 ctx.beginPath(); // 根据配置文件设置相应配置 ctx.lineWidth = config.lineWidth; ctx.strokeStyle = config.strokeStyle; ctx.lineCap = config.lineCap; ctx.lineJoin = config.lineJoin; // 设置画线起始点位 ctx.moveTo(client.endX, client.endY); // 监听 鼠标移动或手势移动 window.addEventListener(mobileStatus ? "touchmove" : "mousemove" , draw); }; // 绘制 const draw = (event) => { // 获取当前坐标点位 const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event; // 超出范围不监听 if (pageY > config.height) { return ; } showTip.value = false ; // 修改最后一次绘制的坐标点 client.endX = pageX; client.endY = pageY; // 根据坐标点位移动添加线条 ctx.lineTo(pageX, pageY); // 绘制 ctx.stroke(); }; // 结束绘制 const cloaseDraw = () => { // 结束绘制 ctx.closePath(); // 移除鼠标移动或手势移动监听器 window.removeEventListener( "mousemove" , draw); }; // 创建鼠标/手势按下监听器 window.addEventListener(mobileStatus ? "touchstart" : "mousedown" , init); // 创建鼠标/手势 弹起/离开 监听器 window.addEventListener(mobileStatus ? "touchend" : "mouseup" , cloaseDraw); // 取消-清空画布 const cancel = () => { // 清空当前画布上的所有绘制内容 ctx.clearRect(0, 0, config.width, config.height); showTip.value = true ; }; // 保存-将画布内容保存为图片 const save = () => { // 将canvas上的内容转成blob流 var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; var lOffset = canvas.width, rOffset = 0, tOffset = canvas.height, bOffset = 0; for (var i = 0; i < canvas.width; i++) { for (var j = 0; j < canvas.height; j++) { var pos = (i + canvas.width * j) * 4; if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) { // 说第j行第i列的像素不是透明的 // 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断 bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标 rOffset = Math.max(i, rOffset); // 找到有色彩的最右端 tOffset = Math.min(j, tOffset); // 找到有色彩的最上端 lOffset = Math.min(i, lOffset); // 找到有色彩的最左端 } } } if (lOffset === config.width && rOffset === 0 && tOffset === config.height && bOffset === 0) { // eslint-disable-next-line ElMessage({ message: "请签名后保存!" , type : "warning" , }); return ; } const trimmedWidth = rOffset - lOffset + 1; const trimmedHeight = bOffset - tOffset + 1; const trimmedCanvas = document.createElement( "canvas" ); trimmedCanvas.width = trimmedWidth; trimmedCanvas.height = trimmedHeight; const trimmedContext = trimmedCanvas.getContext( "2d" ); trimmedContext.putImageData( ctx.getImageData(lOffset, tOffset, trimmedWidth, trimmedHeight), 0, 0 ); const newUrl = trimmedCanvas.toDataURL(); const newImage = new Image(); newImage.src = newUrl; console.log(trimmedWidth < trimmedHeight); if (trimmedWidth < trimmedHeight) { newImage.style.height = "100px" ; newImage.style.transform = "rotate(-" + 90 + "deg)" ; } else { newImage.style.width = "100px" ; } // console.log(newImage.outerHTML + "</img>" ); handleClose(newImage.outerHTML + "</img>" ); }; // 示例文字 let showTip = ref( true ); < /script > |
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签
2020-12-30 promise 学习笔记