恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Vue2+Canvas手写签名组件封装:核心原理与踩坑实践

  • 首页
  • 资讯中心
  • /
  • Vue2+Canvas手写签名组件封装:核心原理与踩坑实践

相关资讯

专利丛林量化指南:从IPC分类到上市公司技术重叠度指标构建 2026/10/15 22:46:54
SpringBoot+Vue3+ElementPlus后台管理系统开发实战:从权限设计到部署优化 2026/10/15 22:46:54
精读风蚀模型综述:从数据需求到有效性验证的完整指南 2026/10/15 22:46:54

最新资讯

COMSOL一维瞬态热传导仿真全流程:建模、求解与解析验证
MaxKB 结合 DeepSeek 实现基于 LLM 大语言模型的知识库问答系统:TaoToken 统一 Key 接入与本地验证
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
Java八种基本类型深度解析:从内存布局到实战选型
AI时代个人量化交易:从想法到实盘的全流程实战指南

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Vue2+Canvas手写签名组件封装:核心原理与踩坑实践

发布时间:2026/10/15 22:46:54
Vue2+Canvas手写签名组件封装:核心原理与踩坑实践 做了半年多的合同签署类项目前端部分最让我头疼的就是签字板。试过直接引第三方签名库发现体积、定制性和移动端适配总有那么几个点不合心意后来干脆用Vue2加canvas自己封装了一个支持手写签名、一键清除、按需下载成图片集成到现有业务里非常顺。这篇文章就把这套组件的设计思路、核心原理和踩坑过程完整梳理一遍给同样需要在Vue2项目里实现签名功能的同学一个可直接参考的落地样板。我默认你已经有Vue2基础也了解canvas的基本API但如果你只是刚入门照着文章里的逻辑一步步来也能跑通。我会把坐标采集、笔迹绘制、高清导出这些关键节点掰开揉碎讲还会附上完整的组件代码思路和实测中遇到的坑方便你直接抄作业后再按自己业务调整。1. 立项前先算账为什么自己封装而不是直接引第三方库先说结论:如果你的需求只是能签个字、能导出图片,那确实没必要自己造轮子,主流签名库开箱即用。但如果你和我一样,业务里有定制笔迹样式、需要跟后端签署流程深度绑定的需求,那自研一个组件会更划算。1.1 第三方签名库的痛点在哪儿市面上能搜到的签名插件,功能确实完整,有的甚至支持笔锋模拟和压感适配。但我实际接入后发现三个绕不开的问题:体积和依赖问题:很多签名库为了兼容各种场景,内部实现很重,有的还自带依赖链。对老项目来说,引入一个几十KB甚至上百KB的插件,心里总觉得不划算。样式的黑盒化:我想把笔迹改成特定颜色、特定粗细,还想在签名区域下面垫一张带水印的背景图。第三方库暴露的配置项虽然不少,但真要改到像素级细节,还是得翻源码、改样式,维护成本反而上去了。与业务结构的割裂:我的场景里,签名完成后不是简单下载图片就完了,而是要拿这张图去合成到合同页面上,还要上传给后端存底。第三方库在这类流程上通常只给你一个getImage()方法,剩下的都得自己拼,那封装程度高反而成了累赘。当然,我也不是说第三方库一无是处。如果你是快速做Demo、时间紧任务重,直接用它绝对省事。自研方案真正的价值在于:你完全掌控每一行代码,后续出了任何问题都能定位到具体逻辑,而不是对着别人的源码干瞪眼。1.2 自研前先列清楚需求边界动手之前,我把需求拆成了一份清晰清单,后面所有设计都围绕这份清单展开:支持PC端鼠标绘制,也支持移动端触摸绘制(这是硬需求,签署场景里用平板签字的人很多)笔迹颜色、粗细、背景色均可配置提供清空、生成图片、下载图片三个核心操作生成的图片要清晰,不能因为canvas尺寸和显示尺寸不一致导致模糊组件内部不依赖任何第三方库,纯Vue2 Canvas实现明确边界的好处是,你不会在开发过程中被各种顺手加个功能的想法带偏。比如当时有同事问能不能在签名板上加个文字水印,我直接拒绝了,因为那应该由业务层去叠加,不该让签名组件变得臃肿。组件只做一件事,并把这一件事做好,这是封装的基本素养。2. canvas签字的核心原理:一次手写动作如何变成一张图在写组件之前,必须先把canvas手写签名的底层机制弄清楚。很多人一上来就写代码,结果发现画出来的线条一顿一顿的,或者歪歪扭扭,就是因为没理解绘制的本质。2.1 事件流与坐标采集:签名板的数据入口签名板的第一件事是捕捉手指或鼠标的轨迹。轨迹由三类事件组成:按下:鼠标mousedown或手指touchstart,表示开始书写移动:鼠标mousemove或手指touchmove,表示持续书写抬起:鼠标mouseup或手指touchend,表示本次书写结束这个事件流是签名板的地基。每次按下,我需要记录一个起点;每次移动,我需要把当前坐标追加到轨迹数组;每次抬起,我结束当前笔画。这里有个最容易出错的地方:坐标的获取。PC端鼠标事件可以直接用event.offsetX和event.offsetY,但移动端的touch事件没有offsetX这个属性,必须用event.touches[0].clientX减去canvas元素的左边界和上边界来计算。所以封装时最好的做法是写一个统一的坐标获取函数,内部判断事件类型,而不是在业务层各写各的。getPosition(event) { const rect this.canvas.getBoundingClientRect() const isTouch event.type.indexOf(touch) ! -1 if (isTouch) { const touch event.touches[0] return { x: touch.clientX - rect.left, y: touch.clientY - rect.top } } return { x: event.offsetX, y: event.offsetY } }注意:这里的getBoundingClientRect()必须在每次事件触发时重新获取,不能只在组件初始化时取一次。因为页面可能有滚动、缩放,canvas的边界位置会变化,缓存值会导致坐标偏移。2.2 为什么用连线而不是画点:连续轨迹的绘制细节初学者最容易犯的错,是在移动事件里用fillRect或者arc直接画一个个小圆点。这种方式有两个致命问题:一是移动事件触发频率高时,圆点之间的间距很明显,画出来的线像虚线;二是点阵方式无法形成平滑曲线,笔迹非常生硬。正确的做法是画线:把每次移动产生的新坐标,和上一次的坐标连接起来。drawLine(startX, startY, endX, endY) { const ctx this.canvas.getContext(2d) ctx.beginPath() ctx.moveTo(startX, startY) ctx.lineTo(endX, endY) ctx.stroke() }看起来简单,但有几个细节决定了成品的质感。第一,lineCap和lineJoin必须设置成round,否则笔画的端点和拐角是尖锐的矩形,和真实笔迹差别很大。第二,移动事件触发频率远低于屏幕刷新率,两次事件之间的位移可能跨越好几个像素,如果不画连线而只画单点,就会出现断裂感。第三,每次画完一段线后不要调用closePath(),因为closePath()会把路径首尾闭合,导致出现多余的线头。2.3 触摸事件与鼠标事件的天壤之别PC端和移动端的事件差异,不只是坐标获取方式不同,事件行为也完全不一样。移动端的touchmove默认会导致页面滚动,尤其在签名这种需要横向大幅度移动的场景里,手指一划,页面跟着滚,签名板根本没法用。解决办法是在touchmove事件上调用event.preventDefault(),并且在绑定事件时传入{ passive: false }参数。Vue2的事件绑定方式也需要注意,如果你用touchmove这种模板语法,默认是没办法传passive:false的,我一般直接在mounted里用原生addEventListener来绑定,顺便解决这个问题。另外,移动端还有一个典型的坑:touchend事件中,touches数组是空的,因为手指已经离开屏幕了。这时候如果要取最后一个坐标点,得用changedTouches[0],否则会报错。所以坐标获取函数里的isTouch分支,需要根据事件阶段区分用touches还是changedTouches。3. 组件设计的关键决策:数据结构、渲染逻辑与对外API需求理清、原理吃透之后,才轮到组件封装。这里我不会给一个完整到能直接复制粘贴的组件文件,而是把几个关键设计决策讲清楚,因为你知道为什么这样设计之后,写代码就是顺水推舟的事。3.1 props设计:哪些配置应该暴露给外部签名板的样式和交互,哪些该由父组件说了算,哪些该是签名板自己的私有逻辑,这是封装时第一个要决策的问题。我最终保留了四个配置项:props名称类型默认值说明strokeStyleString#000000笔迹颜色lineWidthNumber3笔画粗细backgroundColorString#ffffff背景色,默认白色widthNumber500画布逻辑宽度heightNumber200画布逻辑高度你可能注意到我没有把eraserEnabled这类交互模式的开关暴露出来。原因很简单:签名板的核心使用场景是签个字,不是做修图工具,橡皮擦这种能力属于扩展方向,而不是基础能力,放在下一轮迭代再说。我始终遵循一个原则:对外暴露的props越少,组件越容易稳定。3.2 数据结构:如何组织笔迹点集我在组件内部维护了一份完整的数据结构,它是后续所有功能的基石:data() { return { isDrawing: false, currentPoints: [], // 当前正在画的这条笔画的所有坐标点 lastPosition: null, // 上一次的坐标位置 } }想清楚一个问题:按下、移动的过程中,是一边触发事件一边写入canvas,还是先把所有点记下来最后统一绘制?我的选择是:移动过程中实时绘制,同时把点位存储下来。这样做的好处有两个:一是用户能看到即时的笔迹反馈,体验流畅;二是后续如果要实现撤销功能,可以按照笔画为单位回退,每个currentPoints数组就是一条笔画的数据记录。为了让后续的撤销和重做有据可依,我还会在每次touchend或mouseup之后,把currentPoints推入一个strokeHistory数组。这一步可能看起来多余,但真要做历史管理的时候,就知道有多重要了。3.3 公开方法:组件如何向外提供服务Vue2组件通过ref暴露方法。我的签名板对外提供四个方法:clear():清空画布和所有历史记录getImageDataUrl(type image/png, quality 1):返回当前画布内容的Base64字符串getBlob():返回Blob对象,方便直接上传download(fileName):触发浏览器下载这里的download方法我做了个小封装:调用方不需要关心toDataURL的细节,只需要传一个文件名。如果调用方没传文件名,组件会按照signature_${时间戳}的规则自动生成。download(fileName) { const dataURL this.getImageDataUrl(image/png, 1) const link document.createElement(a) link.download fileName || signature_${Date.now()}.png link.href dataURL link.click() }4. 实测中反复踩的三个坑:坐标偏移、画布模糊与滚动穿透代码写完后,真正的战斗才开始。我拿着组件在PC端和移动端分别做了三轮测试,结果被三个问题轮番轰炸。这些问题几乎每个手写canvas组件都会遇到,我把排查过程和解决方案完整还原出来。4.1 坐标偏移:差之毫厘,谬以千里第一个问题是在PC端测出来的。我在canvas左上角写了个起笔测试的文字,然后用鼠标顺着文字边缘画了一圈,结果发现笔画和文字的相对位置是错位的,偏移量在十几像素左右。排查过程是这样的:我先打印了几组mousedown事件的offsetX和offsetY,发现它们和canvas左上角的实际位置对不上。然后我去检查CSS,发现我给canvas设了width: 100%,但canvas的width属性设置的是500。这就是问题所在:canvas的CSS尺寸和内部逻辑尺寸不一致,offsetX是基于CSS尺寸计算的,而绘制坐标是按内部逻辑尺寸来的,两者之间产生了缩放偏差。解决办法有两个方向。一是让canvas的CSS尺寸和逻辑尺寸保持一致,不做响应式缩放;二是做响应式适配时,把坐标做一个等比映射。getPosition(event) { const rect this.canvas.getBoundingClientRect() const scaleX this.canvas.width / rect.width const scaleY this.canvas.height / rect.height // 将CSS坐标转换为canvas内部坐标 return { x: (event.offsetX) * scaleX, y: (event.offsetY) * scaleY } }这个映射逻辑最终被我放进了统一的坐标获取函数里。实际开发中,百分之九十的坐标偏移问题都出在这个CSS尺寸和逻辑尺寸不一致上。4.2 高清屏下的画布模糊问题第二个问题更隐蔽。在我的笔记本上,签名板的显示效果还可以,但放到一台高分辨率的设备上测试时,下载出来的签名图片明显模糊,尤其是笔画边缘有很明显的锯齿感。这里要科普一个概念:Canvas的逻辑像素和物理像素。在普通屏幕上,canvas.width等于其CSS像素尺寸,CSS像素就是物理像素;但在高分屏上,1个CSS像素可能对应2个甚至3个物理像素。如果canvas.width只有500,而CSS尺寸被放大到1000,浏览器就会用这500个逻辑像素去填充1000个物理像素,结果就是模糊。解决方案是让canvas的逻辑尺寸等于CSS尺寸乘以window.devicePixelRatio:initCanvasSize() { const dpr window.devicePixelRatio || 1 const rect this.canvas.getBoundingClientRect() this.canvas.width rect.width * dpr this.canvas.height rect.height * dpr const ctx this.canvas.getContext(2d) ctx.scale(dpr, dpr) }这里最关键的是ctx.scale(dpr, dpr),它把所有绘制坐标都放大了dpr倍,这样画出来的线在物理像素层面是高清的,而绘制时用的坐标依然是CSS像素,业务代码不用做第二次适配。这个方案我在项目里实测下来,下载的图片清晰度提升非常明显。4.3 移动端滚动穿透:签名签成了翻页第三个问题是我在平板设备上测出来的,现象是:手指在签名板上写横笔时,页面也跟着横向滚动,签名轨迹变成了断断续续的折线。我做了三步排查。第一步确认是touchmove的默认行为导致的滚动,第二步是在touchmove里加了event.preventDefault(),第三步是发现只有addEventListener方式才能传passive:false。Vue模板里写touchmove.prevent虽然也能阻止默认行为,但它是通过一个非被动的事件监听器实现的,在某些浏览器上会有性能警告,而且在特定场景下依然会触发滚动,所以我还是选择了原生监听的方式。最终我在mounted里手动绑定了事件,并在beforeDestroy里解绑,避免组件卸载后事件残留导致内存泄漏。mounted() { this.canvas this.$refs.signatureCanvas this.canvas.addEventListener(touchmove, this.handleTouchMove, { passive: false }) }补充一点:为什么不用touchmove.native.prevent?因为Vue的.prevent修饰符本质上是调用了event.preventDefault(),但对于touchmove这种事件,浏览器在创建事件监听器时就已经决定了它是被动还是主动的,模板绑定默认是被动的,没办法通过preventDefault阻止滚动,所以一定要走原生addEventListener这条路径。5. 下载签名的高清化处理:从模糊图片到可商用输出画布绘制已经稳定了,但签名板的使用场景不只是看,还要下载和上传。第三章提到的download方法是简单的toDataURL再加a标签下载,但在实际业务中,我发现这个基础方案有两个需要优化的问题:背景透明和清晰度不可控。5.1 透明背景问题:白底是怎么来的canvas.toDataURL默认导出的图片是透明背景。如果我设置的是白色背景,那没问题;但如果不设置背景色,cavnas本身是透明的,导出的签名图就是一个透明底的黑色笔迹。这在某些场景下是优点(方便叠加到别的背景上),但在合同签署场景里,通常后端需要的是不透明的成品图,否则套在白色合同上还好,套在灰色背景上就会很难看。我给出的方案是离屏Canvas合成。导出的不是当前canvas本身,而是在内存里创建一个新的canvas,先把背景色填上,再把签名canvas的内容绘制上去:exportImage() { const drawCanvas document.createElement(canvas) drawCanvas.width this.canvas.width drawCanvas.height this.canvas.height const drawCtx drawCanvas.getContext(2d) drawCtx.fillStyle this.backgroundColor drawCtx.fillRect(0, 0, drawCanvas.width, drawCanvas.height) drawCtx.drawImage(this.canvas, 0, 0) return drawCanvas.toDataURL(image/png) }这一步看似简单,却解决了透明底带来的后续所有麻烦。签名图拿来直接合成到合同页面上,不用再对背景做处理,后端也能直接留档。5.2 导出尺寸与缩放策略:小画布如何输出大图第4.2节解决了画布模糊问题,但业务里又来了新需求:合同页面上的签名展示区域比较大,如果签名板本身只有500x200,下载出来的图放大后会发虚。这时候有两个策略:一是在初始化时就按目标分辨率设置画布尺寸,比如直接给canvas.width设定为1000,但这样在小屏设备上显示时,边缘会被裁剪,需要配合CSS缩小平铺。二是在导出时按比例放大。这个方案更灵活,签名板保持小尺寸,导出时把离屏canvas设置为目标尺寸,然后再把签名内容绘制上去。第二个方案的注意点是:直接drawImage把一个小canvas拉伸到大canvas上,笔迹依然会模糊,因为源像素不够。所以在绘制时,我采用了整体平移加缩放的方式:先把离屏canvas的坐标系放大到目标尺寸和原始尺寸的比值,再重新执行一遍笔画绘制。这个方案的实现思路是,在导出时遍历strokeHistory里的笔画数据,按照缩放比例重新画一遍:exportImage(scale 2) { const outputCanvas document.createElement(canvas) outputCanvas.width this.canvas.width * scale outputCanvas.height this.canvas.height * scale const outputCtx outputCanvas.getContext(2d) outputCtx.scale(scale, scale) outputCtx.fillStyle this.backgroundColor outputCtx.fillRect(0, 0, this.canvas.width, this.canvas.height) this.strokeHistory.forEach(stroke { outputCtx.beginPath() stroke.forEach((point, index) { if (index 0) { outputCtx.moveTo(point.x, point.y) } else { outputCtx.lineTo(point.x, point.y) } }) outputCtx.stroke() }) return outputCanvas.toDataURL(image/png) }这里必须注意:ctx.scale(scale, scale)设置之后,所有绘制坐标和线宽都会乘以scale。如果你希望导出图的笔迹粗细和原画布一样,那就需要在stroke前把lineWidth除以scale,但这又涉及视觉比例的问题,取决于业务需求。我最终采用的是等比缩放,即笔迹也按比例变粗,这样导出图和原始画布看起来比例一致。5.3 下载之外的另一个动作:签名数据采集下载只是签名的终点之一,很多业务场景里签名数据是要上传到后端的。我在组件里额外提供getBlob方法,将Base64字符串转成Blob对象,方便直接放入FormData:getBlob() { const dataURL this.getImageDataUrl(image/png, 1) const arr dataURL.split(,) const mime arr[0].match(/:(.*?);/)[1] const bstr atob(arr[1]) const n bstr.length const u8arr new Uint8Array(n) while (n--) { u8arr[n] bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) }这段代码是Base64转Blob的通用写法,不难,但每次都要查有点烦,封装进组件后整个上传流程就顺了。配合download方法,签名板既满足本地留档的需求,也满足服务端归档的需求。6. 从能签到好用:五个值得投入的扩展方向基础组件已经稳定运行了,但我在后续使用中还是收到不少反馈,比如能不能让笔迹更好看签错了能不能只撤销一步等等。这里整理几个我实际做过或研究过的扩展方向,按性价比排序。6.1 粗细变化与压感模拟真实的手写笔迹是有粗细变化的,落笔重的地方粗,运笔快的地方细。Canvas原生的lineWidth是固定值,做不了这种变化,但可以通过移动速度来做近似模拟:移动速度越快,笔迹越细;速度越慢(通常意味着用力按压),笔迹越粗。实现思路是记录相邻两次touchmove事件之间的时间差和位移差,算出速度,再根据速度动态调整lineWidth。这个方案虽然不是真压力感应,但视觉上已经非常接近真实笔迹了,成本也不高。6.2 橡皮擦功能如果要在签错的地方局部擦除,可以用globalCompositeOperation destination-out来实现。原理是让后续的绘制操作变成擦除:凡是有笔画经过的地方,像素透明度变0,露出底下的背景色。enableEraser() { const ctx this.canvas.getContext(2d) ctx.globalCompositeOperation destination-out ctx.lineWidth 20 }要注意的是,擦除模式会影响后续的笔迹绘制,用完一定要把globalCompositeOperation设回source-over,否则后面签的字会被擦掉。6.3 撤销与重做在3.2节我提到过strokeHistory数组,它就是为撤销准备的。每次笔画结束,把该笔画的坐标点数组推入历史;撤销时弹栈,然后清空画布并重绘剩余笔画。undo() { this.strokeHistory.pop() this.redrawAll() }这里的代价是:撤销之后要遍历历史里所有笔画重新绘制一遍,笔画多了会有一定的性能损耗。但签名场景里笔画数量通常不多,在移动设备上实测,百级笔画以内重绘都在几十毫秒内完成,可以放心用。6.4 签名图片与业务表单合成签名板本身只负责生成签名图,但业务上经常需要把签名图盖到合同上。这个我建议放到业务层做,不要在签名组件内部实现。原因是合同页面的背景、尺寸、定位规则因业务而异,塞进签名组件只会让组件变得不可复用。业务层拿到签名图后,可以用另一个canvas来做合成:先绘制合同背景图,再按预设坐标把签名图绘制上去,最后导出整张合成图。这样签名组件保持纯粹,业务扩展也自由。6.5 一键清空与二次确认看似小功能,但实际用户最容易误触的就是清空。很多人的第一版实现是点一下立即清空,结果用户还没签完就不小心点到了,整个签名作废。我的建议是加一层二次确认,或者在清空方法里加一个可配置的confirm参数,由调用方决定是否需要确认弹窗。这个细节不属于canvas技术,但对用户体验的影响很大,尤其用于正式合同签署的场景,真的很重要。7. 组件维护过程中的几点体会到这里,整篇文章的核心内容就基本讲完了。最后聊聊我做完这个组件后更深刻的几点体会,也算是对准备做类似封装的你的一些提醒。第一点,canvas不是难点,事件适配才是。这个组件最折腾我的不是绘制逻辑,而是移动端和PC端的事件差异、高分屏的物理像素换算、CSS尺寸和逻辑尺寸的坐标映射。你在设计组件之初,就要把这些跨端因素考虑进去,而不是把PC端跑通了就完事。第二点,封装的边界要克制。签名板的核心职责只有三件事:采集笔迹、展示笔迹、导出图片。任何超出这个边界的业务逻辑,比如合成合同、上传表单、加水印,都应该由调用方去实现。保持组件职责单一,后续维护时你会感谢当初的自己。第三点,数据驱动的思维在canvas组件里同样适用。把每一次落笔抽象成一段点集数组,把多次落笔组织成历史数组,很多高级功能(撤销、重做、回放、导出重绘)就都有了数据的载体。没有这份数据结构,后续的一切扩展都是空中楼阁。第四点,下载图片这个看似简单的功能,要真正用得舒服,必须把透明背景合成、设备像素比适配、按比例缩放这三件事都处理好。否则用户签个字好好的,导出来却模糊发虚、背景发灰,体验一下就垮了。如果用一句话总结我做这个组件的感受,那就是:canvas给了你一块自由画板,但真正决定签名板好不好用的,全在画笔之外的事件处理、坐标系转换和导出策略上。把这几个关键点想清楚、做扎实,组件自然就稳了。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号