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

Vue基于HTML5 video实现自定义视频控制器全攻略

  • 首页
  • 资讯中心
  • /
  • Vue基于HTML5 video实现自定义视频控制器全攻略

相关资讯

基于AIGC的TypeScript脚手架自动化生成方案 2026/10/15 8:55:51
SpringBoot事务事件监听机制详解与应用实践 2026/10/15 8:55:51
SR4MAX维修手册2023-24版:故障码排查与保养参数速查指南 2026/10/15 8:55:51

最新资讯

前端处理Blob音频数据与Wavesurfer集成实践
C30包装称仪表Modbus RTU通讯与PLC集成调试指南
SQLite FTS5 context-mode:轻量级上下文检索实战指南
Java中FTP与SFTP统一接口封装:设计、实现与避坑指南
生化要点总结:用Python构建代谢网络知识库
VSCode插件开发:命令行打包、安装与卸载全流程实操

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

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

本月精选

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

Vue基于HTML5 video实现自定义视频控制器全攻略

发布时间:2026/10/15 8:55:51
Vue基于HTML5 video实现自定义视频控制器全攻略 刚进前端团队那会儿接到一个看起来毫不起眼的任务给项目里的视频播放器换一套控制条。当时产品经理给的要求很简单——“样式好看点功能多一点别用浏览器默认的那套”。我寻思这还不简单找个现成的播放器库不就行了。结果一轮调研下来发现事情远没那么轻松轻量级的库功能不全全功能的重型库又太重而且定制UI时总是被框架的默认样式绑手绑脚。最后索性决定直接用Vue基于HTML5 video原生能力自己实现一套自定义controller。这个决定当时看着只是“退而求其次”实际做完整套方案之后我才意识到这恰恰是最划算、最灵活、也最可控的一条路。这篇文章就完整复盘一下整个实现过程从功能拆解、组件设计、核心交互到踩坑实录把一套可复用的Vue自定义视频控制器的完整方案讲清楚。无论你是刚接触视频播放器开发还是项目里已经遇到定制控制条的需求这篇文章里的思路和代码细节都可以直接参考。1. 为什么放着现成的controls不用非要自己写1.1 原生控件看着省事用起来全是限制HTML5 video标签自带一个controls属性加上之后浏览器就会渲染出一套默认控制条。PC端浏览器里它长一个样移动端浏览器里它又是另一个样在iOS Safari里更是一套完全不同的交互逻辑。最典型的问题有三个。第一个是样式不统一。同一个video元素在Chrome里是深色圆角进度条在Firefox里是浅色扁平风格到了Safari里又全是系统风。如果你的项目有严格的视觉规范比如进度条必须跟整体UI风格一致、控制按钮必须走设计系统里的图标规范那原生控件基本是没法用的。你要么接受它长得不一样要么用一堆浏览器私有伪元素去“磨”样式比如::-webkit-media-controls、::-moz-range-track这些选择器各写各的还随时可能因为浏览器升级而失效。第二个是交互不可控。原生控制条的行为是浏览器定死的你没法决定点击音量图标之后是弹出滑块还是直接静音也没法决定进度条支持多少像素的拖动容错。连“点击进度条跳转到对应时间点”这种基础操作在原生控件里不同浏览器的策略都不完全一样。更不用说像“只允许拖动到已缓冲位置”“视频结束之后隐藏播放按钮”这类的定制逻辑原生控件一概不支持。第三个是样式覆盖容易崩。很多前端同学应该都经历过明明设置了video::-webkit-media-controls { display: none }想自己放一层控制UI上去结果在某些浏览器版本里视频底部还是露出一条默认控制条。这块属于浏览器内部实现不是公开稳定API连“隐藏原生控件”这种操作都存在兼容性风险更别说深度定制了。1.2 成熟播放器库不是不好而是成本在那里既然原生routes走不通很多人的第一反应是用现成的开源播放器比如video.js、ckplayer、jwplayer这类的。坦白说这些库经过多年迭代功能确实完善但同样有它们的问题。最现实的一个问题是体积和复杂度。video.js默认打包加上常用插件几十上百KB是很正常的。如果项目本身只是一个简单的视频展示页整个播放器库的体积可能比页面里其余脚本加起来还大。另一个问题是定制深度。库的API再灵活它的默认结构还是按它的理念设计的你想做“完全自定义的控制条布局”其中一个方向就是关闭它的默认控件然后用它的底层API去渲染自己的UI但这样一来依然是绕了一圈再去操作video元素本身中间还隔着一层抽象。与其这样不如直接用原生video事件加Vue响应式系统自己掌控一切。第三个问题是样式隔离的麻烦。大多数播放器库都有自己的一套CSS为了覆盖它们默认的皮肤样式你还得再写更多的覆盖样式层级一多后面维护就是噩梦。1.3 自定义控制器的价值边界那是不是所有场景都应该自己写控制器也不是。我个人的判断标准很简单如果只是给项目嵌一个视频播放没有任何特殊交互需求直接用video标签加controls属性就够了。如果需求涉及品牌化UI、特殊交互、或者需要深度联动业务数据比如视频进度上报、播放器埋点自己写一套基于Vue的控制器就是性价比最高的方案。自己实现的核心价值有三个第一样式完全由自己掌控视觉与业务系统自然统一第二交互逻辑直接写在Vue组件里可以通过响应式数据驱动方便扩展第三底层就是HTML5 video原生的API和事件不受播放器库本身框架的约束出问题的面小排查起来也直观。2. 功能拆解与方案设计动手写代码之前先把控制条要做的功能拆清楚。一套完整的自定义video controller通常包含下面这些核心模块。2.1 播放/暂停状态同步播放暂停是控制条最基础的功能核心逻辑就是调用video.play()和video.pause()两个方法。但这里有一个重点让控制条上的播放按钮图标跟视频的真实播放状态保持同步。HTML5 video提供了两个关键事件play事件在视频开始播放时触发pause事件在暂停时触发。这两个事件不仅会在你调用play()/pause()时触发还会在用户通过右键菜单、浏览器原生控制条、甚至其他脚本控制视频时触发。所以正确做法是把这两个事件作为唯一的状态来源用Vue的响应式变量isPlaying去承接播放状态按钮图标渲染完全依赖这个变量而不是依赖你“调用过”什么方法。2.2 进度条与当前时间定位进度条是控制器里交互最复杂、也最容易出bug的部分。它至少包含三个需要联动的要素当前播放进度、视频总时长、用户通过进度条做时间跳转。当前播放进度的数据来源是timeupdate事件这个事件在播放过程中会高频触发平均每秒约4次。总时长来自loadedmetadata事件只有元数据加载完成之后duration才是有效数值。时间跳转则要处理两类用户操作点击进度条和按住拖动进度条。这两个交互视觉上很容易写成一套逻辑但实现上有本质区别。点击是瞬时行为在mouseup的位置换算成时间然后直接赋值currentTime即可拖动则是连续行为拖拽过程中进度条UI要随时变化但currentTime的赋值不能跟得太紧否则视频画面和进度条的反馈会产生明显的“互相追赶”感。2.3 时间显示与缓冲进度时间显示这块通常展示的是“当前时间 / 总时长”的格式。当前时间只需要在timeupdate里更新总时长从loadedmetadata拿。这里有一个格式化细节要走秒数要先经过Math.floor取整再转成mm:ss格式否则进度播放中时间显示会出现0.4秒、1.2秒这样的跳变观感很差。缓冲进度条是很多自定义实现里容易被忽略的。它表现的是用户可拖动的范围数据来源是video的buffered属性配合progress事件更新。监听progress事件后用buffered.end(buffered.length - 1)取到当前缓冲的末尾时间点除以duration再换算成百分比宽度。这个数值在网络状况变化时会更新所以在进度条上增加一层动态宽度的缓冲条能非常直观地提升整体体验。2.4 音量与静音控制音量控制的基础API是video.volume取值范围是0到1video.muted是布尔值控制静音状态。这里要特别提醒volume的赋值在不同浏览器里对小数位数的处理不一致建议在赋值前用parseFloat并且限制到两位小数以内。交互层面音量控制通常包含一个音量按钮和一个音量滑块。点击音量按钮时做一次状态切换如果当前非静音就记录音量值并置为0muted设为true同时音量滑块归零如果处于静音就恢复之前记录的音量值取消静音。这个“记录音量值再静音”的细节很关键不会因为静音一次就永久丢失用户的音量设置。2.5 全屏与画中画全屏分两种元素全屏和浏览器全屏。video元素本身有requestFullscreen()方法任何元素都可以调用这个API进入浏览器的全屏模式。退出用document.exitFullscreen()。需要注意video在元素全屏下样式默认是“保持元素自身的宽高比并撑满屏幕”这块要配合CSS控制object-fit和全屏时的背景色。画中画Picture-in-Picture是一个独立于全屏的功能调用video.requestPictureInPicture()进入画中画模式document.exitPictureInPicture()退出。这个API在Chrome和Edge上支持Android对移动端iOS的支持较差所以功能开关上要做好兼容检测不支持时干脆不渲染入口按钮避免用户点击无反应。2.6 倍速播放HTML5 video自带playbackRate属性支持范围内的任意数值。常见的倍速选项是0.5、0.75、1、1.25、1.5、2这六档。界面上做成一个循环切换按钮也行做成浮层菜单也好。需要注意的是修改playbackRate后播放不会中断但播放画面可能出现轻微卡顿这是浏览器解码策略决定的不是实现问题。倍速状态建议在页面里做一次持久化用户换视频时仍然保留上次的选择。3. 基于Vue的落地实现功能理清楚之后就可以动手搭建具体实现了。下面这部分的代码基于Vue3的组合式API来写如果你还在用Vue2思路完全一致把setup里的逻辑对应到data、computed、watch和methods即可。3.1 组件结构设计一个可控、可维护的自定义播放器组件应该把控制条拆成独立子组件而不是堆在一个巨大的文件里。我的做法是这样分层PlayerContainer.vue负责承载video元素、播放器容器、控制条整体布局核心状态管理在这里。VideoController.vue控制条主体负责按钮布局和数据向上传递。ProgressBar.vue进度条组件只负责进度UI与时间跳转交互。VolumeControl.vue音量控制组件包含音量按钮和音量滑块。TimeDisplay.vue时间显示组件输入秒数输出格式化文本。PlaybackRateMenu.vue倍速选择组件。这种拆分不是为了让文件数量看起来多而是为了让每个组件的职责单一。比如进度条组件内部不需要知道音量逻辑音量控制也不关心全屏状态。状态通过props向下传递事件通过emit向上抛层级关系干净后面定位bug会省很多时间。3.2 播放控制核心逻辑先看父组件里video元素与播放状态管理的核心代码。template div refplayerContainer classplayer-container video refvideoEl classplayer-video :srcsrc playhandlePlay pausehandlePause timeupdatehandleTimeUpdate loadedmetadatahandleLoadedMetadata progresshandleProgress endedhandleEnded clicktogglePlay /video VideoController :is-playingisPlaying :current-timecurrentTime :durationduration :buffered-percentbufferedPercent :volumevolume :mutedmuted :rateplaybackRate toggle-playtogglePlay seekhandleSeek change-volumehandleVolumeChange toggle-mutetoggleMute toggle-fullscreentoggleFullscreen change-ratehandleRateChange / /div /template在script里管理状态和事件监听。这里用computed把当前时间的百分比算好传给进度条组件做UI渲染。script setup import { ref, computed } from vue; const props defineProps({ src: { type: String, required: true } }); const videoEl ref(null); const playerContainer ref(null); const isPlaying ref(false); const currentTime ref(0); const duration ref(0); const bufferedPercent ref(0); const volume ref(1); const muted ref(false); const playbackRate ref(1); const progressPercent computed(() { if (!duration.value) return 0; return (currentTime.value / duration.value) * 100; }); function togglePlay() { if (!videoEl.value) return; if (videoEl.value.paused) { videoEl.value.play(); } else { videoEl.value.pause(); } } function handlePlay() { isPlaying.value true; } function handlePause() { isPlaying.value false; } function handleTimeUpdate() { currentTime.value videoEl.value.currentTime; } function handleLoadedMetadata() { duration.value videoEl.value.duration; } function handleProgress() { const buffered videoEl.value.buffered; if (buffered.length 0) { bufferedPercent.value (buffered.end(buffered.length - 1) / videoEl.value.duration) * 100; } } function handleEnded() { isPlaying.value false; } function handleSeek(time) { videoEl.value.currentTime time; currentTime.value time; } /script这版核心逻辑里最需要注意的点是“状态源头的唯一性”。isPlaying这个变量只通过play/pause/ended这三个video事件驱动togglePlay里只在video元素上调用play/pause方法不做任何额外的状态赋值。这个设计的核心原因是video元素本身的状态才是真实状态任何外部变量都只是它的镜像强行在函数里同步赋值容易造成状态不一致。实测中常见的翻车场景是用户在播放过程中切换浏览器标签页浏览器可能会自动暂停video此时如果只依赖函数里手写状态切换按钮图标就会显示“播放中”画面却是暂停的。用事件驱动状态之后无论视频被什么原因暂停pause事件都会触发图标立刻跟随变化。3.3 进度条交互实现进度条是整个控制器里最容易写砸的部分先看组件实现。template div classprogress-wrapper refprogressWrapper mousedownhandleMouseDown mousemovehandleMouseMove mouseuphandleMouseUp div classprogress-track/div div classprogress-buffer :style{ width: bufferedPercent % }/div div classprogress-filled :style{ width: progressPercent % }/div div classprogress-thumb :style{ left: progressPercent % }/div /div /template在vue3的setup里进度条交互的关键逻辑是这样处理的script setup import { ref } from vue; const props defineProps({ progressPercent: { type: Number, default: 0 }, bufferedPercent: { type: Number, default: 0 } }); const emit defineEmits([seek]); const progressWrapper ref(null); const isDragging ref(false); function getSeekTime(mouseX) { const rect progressWrapper.value.getBoundingClientRect(); const percent (mouseX - rect.left) / rect.width; return Math.max(0, Math.min(1, percent)); } function handleMouseDown() { isDragging.value true; document.addEventListener(mousemove, handleMouseMove); document.addEventListener(mouseup, handleMouseUp); } function handleMouseMove(event) { if (!isDragging.value) return; const percent getSeekTime(event.clientX); emit(seek, percent * props.duration); } function handleMouseUp() { if (!isDragging.value) return; isDragging.value false; document.removeEventListener(mousemove, handleMouseMove); document.removeEventListener(mouseup, handleMouseUp); } /script这段逻辑里有一个很容易被忽略的细节mousedown是加在进度条容器上的但mousemove和mouseup要挂在document上不能挂在容器上。原因是当拖动的速度快过浏览器监听速率时鼠标指针可能短暂离开进度条容器如果mousemove挂在容器上就会松手之后丢失后续的move事件进度条跟到一半直接卡住。挂在document上只要鼠标还在浏览器窗口内都不会丢事件。另一个关键点拖动过程中emit的seek事件父组件里做了currentTime的更新。但是这里要注意只更新video.currentTime不要在这里手动调video.play()。很多实现犯的错误是拖动完直接更新currentTime就让视频自动播放但实际上用户只想找一个时间点并不想立刻播放。更合理的交互是只做时间跳转保持当前的播放状态不变。3.4 音量、全屏与时间格式化音量控制组件内部有一个滑块但音量值的最终修改还是要通过父组件。script setup const props defineProps({ volume: { type: Number, default: 1 }, muted: { type: Boolean, default: false } }); const emit defineEmits([change-volume, toggle-mute]); function handleVolumeInput(event) { const val parseFloat(event.target.value); emit(change-volume, val); } function handleMuteToggle() { emit(toggle-mute); } /script父组件里对应的处理函数function handleVolumeChange(val) { videoEl.value.volume val; if (val 0) { videoEl.value.muted false; } volume.value val; muted.value videoEl.value.muted; } function toggleMute() { if (videoEl.value.muted) { videoEl.value.muted false; // 恢复静音前的音量 videoEl.value.volume volume.value || 0.5; } else { // 记录当前音量后静音 volume.value videoEl.value.volume; videoEl.value.volume 0; videoEl.value.muted true; } muted.value videoEl.value.muted; }时间格式化这个活儿不复杂但属于细节决定体验的典型。核心函数function formatTime(seconds) { if (!isFinite(seconds) || seconds 0) { return 00:00; } const s Math.floor(seconds % 60); const m Math.floor(seconds / 60); return ${String(m).padStart(2, 0)}:${String(s).padStart(2, 0)}; }全屏处理这里单独说一下。调用requestFullscreen()之后video元素会被浏览器接管铺满全屏。此时控制条是需要继续显示的但它的position是相对于父容器的在全屏模式下父容器变成了整个屏幕所以控制条依然可见这点不用担心。容易忽略的是退出全屏时的样式恢复要监听fullscreenchange事件并维护一个isFullscreen状态用来切换全屏按钮的图标。同时要提前做一下特性检测不要在所有浏览器里都调用requestFullscreen。老一些的浏览器实现前缀不同比如webkitRequestFullscreen建议用通用的能力判断再调用。3.5 样式实现与体验细节控制条的样式设计核心原则是“常态够简洁交互有反馈”。结构上用absolute定位把控制条放在video元素底部的渐变黑条背景上但不要遮挡视频主体区域。一套能直接用、观感不错的样式基础如下.player-container { position: relative; width: 100%; background: #000; } .player-video { width: 100%; display: block; } .video-controls { position: absolute; left: 0; right: 0; bottom: 0; padding: 8px 12px; display: flex; align-items: center; gap: 10px; background: linear-gradient(transparent, rgba(0, 0, 0, 0.75)); }这套样式有几个实测中要注意的点控制条不要直接用纯黑背景用从透明到半透明的渐变这样视频内容展示完整度更好。进度条的progress-wrapper高度不要做太大交互热区可以大一点但可视区域建议控制在4-6px视觉上更精致。或者用padding在视觉高度外扩展热区。控制条的按钮元素建议用垂直居中的flex布局图标统一尺寸否则不同图标存在时会有明显高度抖动。如果有“播放几秒后控制条自动隐藏”的需求可以在鼠标移入容器时显示移出后延迟2秒左右隐藏。但要注意在键盘焦点进入控制条内部时不应该隐藏否则会影响键盘用户的可用性。这个细节挺考验产品细心程度能做好就很加分。4. 实战踩坑与问题排查编写过程不可能一遍过这部分整理一些实际开发中遇到的典型问题以及对应的排查方向对直接跳过前面部分看结论的人来说也值得先读这一段。4.1 duration拿到的却是NaN很多初次写进度条的同学会遇到时间显示一直00:00 / 00:00但视频明明在播放。原因大概率是dataloadedmetadata还没有触发时就去读duration了。video元素的metadata加载是异步的刚挂载src的那一刻duration是NaN只有等元数据就绪loadedmetadata事件触发之后才有准确值。一个容易踩的连环坑如果src后面动态变化比如在播放器里切换了视频源有些浏览器不会重新触发loadedmetadata而是直接复用旧的duration。此时需要在reset逻辑里先清空duration设置src后再重新等待事件。排查时可以随手在控制台打印video.duration、video.readyState这两个值判断是还没加载完还是事件监听丢失。4.2 点击进度条和拖动进度条的逻辑区分这个问题非常典型。实现如果写得太粗糙用户快速点击进度条时会意外触发mousedownmovement逻辑出现“点一下跳了两次”或者“点击却触发了拖动”的问题。最好的做法是引入一个“位移阈值”概念在mousedown时记录起始坐标在mousemove中只有当鼠标位移超过一定阈值比如5px时才进入“拖动模式”在此之前发生的mouseup应该当作一次点击处理。这样既能保证拖动的流畅又不影响点击的精准。4.3 播放结束时控制台状态错乱视频播放结束后video会触发ended事件这个时候currentTime定格在durationisPlaying变成false。如果控制条实现里没有处理ended最直接的后果是视频结束后还显示一个大大的播放按钮点了也没反应因为视频已经结束了。正确的处理是在ended事件里把isPlaying置为false同时可以做一件事——把视频“拉回开头”或者显示一个重播按钮。常规操作是把currentTime置为0这样用户点击播放按钮时视频从起点重新播放。这个交互业内比较统一。4.4 动态换个视频源播放器状态没有重置播放器通常不只播一个视频经常伴随视频列表或者切换下一集的需求。这里最稳妥的做法是watch到主组件里的src变化然后执行一个reset方法把这个模块里维护的所有状态全部清零isPlaying、currentTime、duration、progressPercent、bufferedPercentvideo的currentTime本身也要清掉。如果不做状态reset就会遇到典型的“切换视频后进度条还显示上一个视频的进度”的bug。而且这个问题在本地开发时不容易发现因为不少测试视频时长长度类似一旦换上长度差很大的真实片源bug立刻暴露。4.5 阻塞的低频timeupdate事件timeupdate虽然是高频事件但在有些情况下它的触发频率会掉到每秒1次甚至更低比如页面被切到后台、视频本身被浏览器节能策略限制的时候。如果你的进度条UI只在timeupdate里更新就会出现用户切走看一眼别的标签页、再回来时进度条突然跳了一大段的情况。处理办法有两条路一是用setInterval做保底刷新比如每250毫秒同步一次当前时间和进度条二是在用户进行拖动时不要依赖timeupdate更新进度条UI而是直接用拖动事件里的值更新。每一条都值得在代码里落实。尤其是在拖动过程中如果进度条UI靠timeupdate地更新拖起来会明显发卡。5. 一些实操层面的加分项功能跑通之后真正区分播放器实现好坏的是细节层面的一些加分项。这些不会写在基础需求里但缺了它们整体体验就是差那么一口气。第一个是键盘快捷键支持。空格键切换播放暂停左右方向键控制快进快退上下方向键控制音量。这是用户一旦用习惯了就回不去的功能实现成本也不高在容器元素上挂一个keydown监听就行注意preventDefault掉空格键的默认滚动行为。第二个是视频播放的防误触。在移动端浏览器video会自动触发全屏播放这是iOS Safari的老规矩。如果你做的是移动端H5页面可以在video上添加controlslist和playsinline属性其中playsinline对iOS尤其关键。注意这个方案并不是100%对所有iOS版本都生效还是要实际以真机测试结果为准。第三个是数据上报。在timeupdate事件中可以用一个throttle比如每20秒记录一次完整的播放状态当前时间、播放状态、倍速、缓冲比例。这些数据对了解真实用户的播放完成度、卡顿位置非常有价值。第四个是播放器尺寸的响应式处理。默认控制条在窄屏下容易挤成一团需要在合适的断点下把部分次要控制项隐藏比如倍速按钮和音量滑块改用图标触发展开。这块没有银弹用flex-wrap配合媒体查询处理是最朴素有效的方式。6. 写在后面的体会整套方案实现下来我最真实的感受是自定义播放器controller不是一个“能不能做”的问题而是“愿不愿意花心思把边界情况都处理好”的问题。功能列表上看得见的按钮、滑条、时间只是冰山一角真正的工作量在看不见的地方事件监听的场景隔离、状态同步的一致性、异常边界值的兜底。最后分享一个非常实用的小技巧开发过程中给自己留一个“调试控制台”把video元素的所有关键事件打印出来观察事件触发顺序。花了很长时间都没定位的bug很多时候用原生事件日志一看就能找到原因。这也是我推荐大家从底层自己实现的原因因为你对每个事件、每个状态的来源都会非常熟悉排查问题的时候比隔着一层播放器库的封装要清晰得多。

关于恒美微站

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

快速链接

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

服务项目

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

联系方式

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

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