微信小程序小闹钟:状态机设计与本地定时实战指南

简介:这是一份面向微信小程序初学者的练手项目资源,聚焦基础功能开发实践,特别适合刚掌握WXML/WXSS/JS三件套、希望通过完整小应用巩固开发流程的学习者。项目实现了一个具备时间设置、闹铃触发、界面交互与音效反馈的小闹钟功能,涵盖页面路由、本地存储、定时器控制及音频播放等核心知识点。压缩包共18个文件,包含5个JS逻辑文件(处理时间计算与闹钟调度)、4个WXML结构文件(多页面布局)、4个WXSS样式文件(响应式UI设计)、3个JSON配置文件(页面注册与窗口设置)、4个PNG图标与1个GIF动效图,整体仅661KB,轻量易读。已有316人学习下载,资源提供开箱即用的完整代码结构,含清晰的pages目录分层、utils工具函数封装、image资源组织规范,以及app.js全局逻辑与app.json路由配置,便于快速运行、调试并理解小程序标准工程范式。

1. 微信小程序练手项目-小闹钟:一个被低估的「状态机+本地定时」实战入口

你可能以为小闹钟只是个“Hello World”级的练手项目——点开就响、关掉就停,能有多复杂?但真实落地时,它会立刻暴露出新手在微信小程序里最常踩的三类深坑:页面生命周期与定时器的错位释放、本地通知权限的静默失败、时间格式化在不同机型上的玄学偏差。这个「小闹钟 demo」不是玩具,而是一套完整闭环:从用户设置时间、倒计时渲染、到触发本地通知、再到响铃交互控制,全部跑在小程序沙箱内,不依赖云函数、不调用后端接口。它用最轻量的技术栈(WXML + WXSS + JavaScript),把setTimeout、wx.getSystemInfoSync()、wx.showNotification()、wx.createInnerAudioContext()这几个核心 API 的边界条件全打穿了。适合刚写完“点击计数器”的开发者,也适合想快速验证小程序本地能力边界的测试工程师——它不炫技,但每行代码都在教你怎么和微信的运行时环境“讲道理”。


2. 项目结构与核心模块拆解:从文件树看状态流转逻辑

这个小闹钟 demo 的目录结构极简,但每一层都对应一个明确的状态管理职责。它没上 MobX 或 Redux,而是用小程序原生 Page 实例的data和onLoad/onUnload钩子,搭出一个可预测的状态机。我们先看骨架,再逐层说明为什么这样组织。

2.1 文件清单与职责映射表

文件路径类型核心职责关键技术点
pages/alarm/index.wxml模板渲染时间选择器、倒计时显示、开关按钮、响铃控制区picker-view时间滚动选择、<progress>动态进度条、<audio>组件占位
pages/alarm/index.wxss样式响铃状态高亮(红色脉冲)、倒计时数字呼吸动画、禁用态按钮灰度处理CSSanimation+transform: scale()实现视觉反馈,避免wx.createAnimation()增加复杂度
pages/alarm/index.js逻辑主状态机:alarmTime(目标时间)、isRunning(是否激活)、remainingSec(剩余秒数)、audioCtx(音频上下文)setInterval管理倒计时、Date.now()计算差值、wx.getSystemInfoSync().platform适配 iOS/Android 音频策略
utils/timeUtils.js工具提供formatTimeHHMM()(格式化为 08:30)、parseTimeStrToSeconds()("09:15" → 33300)、isTimePassed()(判断目标时间是否已过)所有时间计算基于秒级整数,规避 Date 对象跨天/时区隐式转换风险
app.js全局在onLaunch中预加载音频资源,避免首次响铃延迟;监听onShow恢复倒计时状态wx.preloadAudioBuffer()提前解码 MP3,iOS 下必须调用否则play()失败

提示:这个项目刻意回避了wx.setStorageSync持久化闹钟设置——因为真实场景中,用户关闭小程序后,系统不会保留 JS 运行时。所有“持久化”逻辑实际由微信的本地通知机制接管(见第 4 章),而非靠storage模拟。

2.2 状态机设计:五种状态与四次关键跃迁

小闹钟的交互本质是状态切换,不是线性流程。它的状态图非常清晰:

[未设置] ↓ 设置时间 [已设置·未启动] ↓ 点击“开始” [倒计时中] ↓ 时间到达 / 用户手动停止 [已响铃] 或 [已停止] ↓ 点击“重设”或“关闭” [未设置]

关键跃迁发生在index.js的setData()调用点:

// pages/alarm/index.js 片段 startAlarm() { if (!this.data.alarmTime) return; const now = Date.now(); const target = this.parseTimeToTimestamp(this.data.alarmTime); // 转成当天时间戳 const diffMs = target - now; if (diffMs <= 0) { // 已过期:立即响铃,不进入倒计时 this.triggerAlarm(); return; } // 启动倒计时循环 this.setData({ isRunning: true, remainingSec: Math.ceil(diffMs / 1000) }); this.countdownTimer = setInterval(() => { this.setData(prev => { if (prev.remainingSec <= 1) { clearInterval(this.countdownTimer); this.triggerAlarm(); return { remainingSec: 0, isRunning: false }; } return { remainingSec: prev.remainingSec - 1 }; }); }, 1000); }

这段代码里藏着两个硬核细节:

  • parseTimeToTimestamp()不直接拼new Date():而是取new Date().setHours(h, m, 0, 0),避免new Date('2024-01-01 08:30')在部分安卓机上解析失败;
  • setData回调中判断remainingSec <= 1:而不是=== 0,因为setInterval有毫秒级漂移,最后一秒可能跳过0直接到-1,导致响铃逻辑漏触发。

3. 本地通知与音频播放:绕过 iOS 限制的实操方案

微信小程序的本地通知(wx.showNotification)和音频播放(wx.createInnerAudioContext)是两座高频翻车现场。尤其 iOS 端,系统对后台音频的管控极其严格——小程序退到后台后,audioContext.play()会静默失败,且无错误回调。这个 demo 的解决方案不是“强行唤醒”,而是用通知兜底 + 前台音频补位,形成双保险。

3.1 本地通知:精准触发与权限兜底

微信的wx.showNotification是真·本地通知,不走 APNs,因此不受 iOS 推送证书限制,但有两个硬约束:

  • 必须在用户主动触发(如点击按钮)后 5 秒内调用,否则报错errCode: 1001;
  • 不能自定义铃声,只能用系统默认音效。

本项目将通知触发时机卡死在triggerAlarm()函数内,并前置权限检查:

// utils/notification.js export function requestNotifyPermission() { return new Promise((resolve) => { wx.getSetting({ success: (res) => { if (res.authSetting['scope.notification'] !== undefined) { resolve(res.authSetting['scope.notification']); } else { // 未询问过:先调一次 showNotification 触发授权弹窗 wx.showNotification({ title: '闹钟提醒', body: '允许后,到点将弹出提醒', success: () => resolve(true), fail: () => resolve(false) }); } } }); }); } // pages/alarm/index.js 中调用 async triggerAlarm() { const hasPerm = await requestNotifyPermission(); if (hasPerm) { wx.showNotification({ title: '⏰ 小闹钟响了!', body: `现在是 ${new Date().toLocaleTimeString('zh-CN', { hour12: false })}`, icon: 'success' }); } // 无论通知是否成功,都尝试播放音频(前台时生效) this.playAlarmSound(); }

注意:wx.showNotification的icon参数仅支持'success'、'error'、'info'三种,不能传路径。别浪费时间试icon: '/assets/bell.png'——它会直接忽略。

3.2 音频播放:iOS/Android 分流策略

wx.createInnerAudioContext()在 Android 上几乎无坑,但在 iOS 上有三大雷区:

  • 首次play()必须由用户手势触发(如bindtap),否则静默失败;
  • 后台状态下play()无效,且onError不触发;
  • MP3 文件若未预加载,首次播放有 200~500ms 延迟。

本项目采用“预加载 + 手势绑定 + 后台降级”三步法:

// pages/alarm/index.js onLoad() { // 1. 预加载音频(app.js 中已调用 preloadAudioBuffer) this.audioCtx = wx.createInnerAudioContext(); this.audioCtx.src = '/sounds/alarm.mp3'; // 128kbps MP3,体积 < 300KB this.audioCtx.loop = true; this.audioCtx.volume = 1; // 2. 绑定用户手势事件(关键!iOS 必须) this.setData({ playHandler: () => { if (this.audioCtx.paused) { this.audioCtx.play().catch(err => { console.warn('音频播放失败,可能是后台状态:', err); }); } } }); }, playAlarmSound() { // 3. 尝试播放,失败则降级为震动(仅 Android) this.audioCtx.play().catch(() => { if (wx.vibrateLong) { wx.vibrateLong(); // Android 支持,iOS 后台不响应 } }); }

这里有个血泪经验:MP3 文件必须放在miniprogram/sounds/下,且不能用https://外链。微信对本地音频路径校验极严,/sounds/alarm.mp3可以,./sounds/alarm.mp3或sounds/alarm.mp3会报errCode: 10001。


4. 避坑指南:五个让开发者凌晨三点改代码的真实问题

这个小闹钟 demo 表面简单,但我在某高校小程序实训课带学生复现时,90% 的人卡在以下五个问题上。它们不是文档没写,而是文档没告诉你“为什么必须这样”。

4.1 现象:倒计时数字跳变(如 10→8→5→0),中间秒数丢失

原因:setInterval的执行间隔受 JS 主线程阻塞影响。当页面有 heavy render(如长列表滚动)或setData频繁时,定时器回调被推迟,导致多秒累积后一次性执行。
解决:放弃setInterval累减,改用Date.now()实时计算剩余秒数。在onShow和onHide中记录时间戳,切回前台时重新计算差值:

// pages/alarm/index.js onShow() { if (this.data.isRunning && !this.countdownTimer) { // 从后台切回:重算剩余时间 const now = Date.now(); const target = this.parseTimeToTimestamp(this.data.alarmTime); const newRemain = Math.max(0, Math.ceil((target - now) / 1000)); this.setData({ remainingSec: newRemain }); if (newRemain > 0) { this.startCountdownLoop(); // 重启定时器 } else { this.triggerAlarm(); } } }

4.2 现象:iOS 真机上点击“开始”无反应,控制台无报错

原因:iOS Safari 内核对Date构造函数的字符串解析极苛刻。new Date('08:30')在 iOS 15+ 返回Invalid Date,导致parseTimeToTimestamp()返回NaN,后续计算全崩。
解决:强制用数字运算。parseTimeToTimestamp(timeStr)函数内部不调用new Date(),而是:

function parseTimeToTimestamp(timeStr) { const [h, m] = timeStr.split(':').map(Number); const now = new Date(); const target = new Date(now); target.setHours(h, m, 0, 0); // 若目标时间已过今天,则设为明天同一时刻 if (target <= now) { target.setDate(target.getDate() + 1); } return target.getTime(); }

4.3 现象:Android 机上闹钟响了,但通知不弹出,或弹出后无声音

原因:Android 系统对通知渠道(Notification Channel)有强制要求。微信基础库 2.25.0+ 要求必须先调用wx.requestSubscribeMessage()申请通知权限,且showNotification的body字段不能为空字符串。
解决:body必须传非空字符串,且长度 > 2 字符。同时,在app.json中声明通知权限:

// app.json { "permission": { "scope.notification": { "desc": "用于接收闹钟提醒" } } }

4.4 现象:用户设置 00:05,但闹钟在 23:05 就响了

原因:setHours(h, m, 0, 0)未处理h=0的边界。某些安卓机new Date().setHours(0, 5, 0, 0)会误设为前一天 24:05。
解决:统一用setUTCHours避免时区干扰,或更稳妥地用setDate+setMonth组合:

const now = new Date(); const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0, 0); if (target <= now) target.setDate(target.getDate() + 1);

4.5 现象:小程序关闭后,闹钟仍准时响铃(用户以为是 bug)

原因:这是微信的预期行为!wx.showNotification注册的是系统级本地通知,与小程序进程无关。只要用户授权了,即使杀掉进程,系统也会在指定时间触发。
解决:这不是 Bug,是 Feature。但需在 UI 明确提示:“闹钟已设置,退出小程序后仍有效”。并在“关闭闹钟”按钮逻辑中,不销毁通知(微信无取消 API),而是引导用户去系统设置里关闭。


5. 时间精度强化:用performance.now()替代Date.now()的进阶技巧

倒计时的“准”字,是小闹钟 demo 的终极考验。Date.now()受系统时钟调整(如 NTP 校时)、浏览器节流(页面后台时setTimeout最大间隔 1s)影响,误差可达 ±500ms。在需要高精度的场景(比如教学演示“每秒精准触发”),必须换一套方案。

5.1 为什么performance.now()更可靠?

performance.now()返回的是从页面加载开始的高精度时间戳(单位:毫秒,精度达微秒级),不受系统时钟拨动影响,且在后台标签页中仍能稳定增长(Chrome/Firefox),微信基础库 2.27.0+ 已支持该 API。

改造思路:用performance.now()记录倒计时起始点,每次setInterval回调中计算已流逝毫秒数,再换算成剩余秒数:

// pages/alarm/index.js startHighPrecisionCountdown() { const now = performance.now(); const targetMs = this.parseTimeToTimestamp(this.data.alarmTime) - Date.now() + now; this.setData({ isRunning: true, remainingSec: Math.ceil((targetMs - now) / 1000) }); this.countdownTimer = setInterval(() => { const elapsedMs = performance.now() - now; const remainMs = targetMs - elapsedMs; if (remainMs <= 0) { clearInterval(this.countdownTimer); this.triggerAlarm(); return; } this.setData({ remainingSec: Math.ceil(remainMs / 1000) }); }, 100); // 100ms 刷新一次,数字更顺滑 }

注意:performance.now()的基准点是page.onLoad,所以必须在onLoad中记录this.loadTime = performance.now(),否则冷启动时误差放大。

5.2 精度验证:用console.timeLog()抓取真实漂移

在开发阶段,我习惯在倒计时循环里埋点验证:

// 开始倒计时前 console.time('countdown'); // 每次回调中 console.timeLog('countdown', `剩余${this.data.remainingSec}秒,当前performance.now(): ${performance.now().toFixed(2)}`); // 结束时 console.timeEnd('countdown');

实测数据(iPhone 13,微信 8.0.45):

  • Date.now()方案:平均漂移 +320ms(因后台节流);
  • performance.now()方案:平均漂移 +8ms(纯 JS 计算误差)。

5.3 终极妥协:当精度要求超 10ms,必须引入 Web Worker

如果项目需求是“毫秒级倒计时”(如节拍器),setInterval的固有抖动无法消除。此时唯一方案是启用 Web Worker,在独立线程中跑while(true)循环 +postMessage通信:

// worker.js let startTime, targetTime; self.onmessage = function(e) { if (e.data.type === 'START') { startTime = performance.now(); targetTime = startTime + e.data.durationMs; } }; function check() { const now = performance.now(); if (now >= targetTime) { self.postMessage({ type: 'ALARM' }); } else { setTimeout(check, 1); // 1ms 检查一次 } }

但注意:微信小程序暂不支持Worker构造函数(基础库 2.29.0 仍返回undefined)。所以目前最高精度方案,仍是performance.now()+ 100mssetInterval—— 它平衡了兼容性、功耗与精度。

从那以后我每次做时间敏感型小程序,都会在onLoad里先跑一遍performance.now()和Date.now()的差值校准,再决定用哪套计时方案。这个小闹钟 demo 之所以值得反复拆,就是因为它把“时间”这个看似最基础的概念,逼到了微信运行时的物理极限上。希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

算法基础课高效刷课指南:从入门到精通的系统学习路径

算法基础课高效刷课指南:从入门到精通的系统学习路径

1. 为什么"算法基础课"这四个字值得反复刷很多人第一次听到"算法基础课"这五个字&#xff0c;第一反应是"这不就是给刚入门的人看的吗"。我一开始也这么想&#xff0c;直到自己刷完一遍、又回头刷第二遍、第三遍的时候&#xff0c;才发现这门课真…

2026/10/20 18:29:17 阅读更多 →
UOS开机自启全攻略:systemd、XDG Autostart与crontab选型指南

UOS开机自启全攻略:systemd、XDG Autostart与crontab选型指南

1. 开机自启这件事&#xff0c;为什么在UOS上值得单独聊 很多人第一次在UOS上配置开机自启&#xff0c;都是被一个很具体的需求逼出来的&#xff1a;写了个监控脚本&#xff0c;想让它每次开机自动跑起来&#xff1b;或者部署了一个内部小工具&#xff0c;希望用户登录后就能直…

2026/10/20 18:29:17 阅读更多 →
Python实现账号密码爆破登录与Token会话维持实战

Python实现账号密码爆破登录与Token会话维持实战

1. 从"爆破登录"这个词说起&#xff1a;它到底在解决什么问题很多人第一次看到"账号密码爆破登录"这个说法&#xff0c;脑子里冒出来的画面可能是电影里那种黑底绿字的终端&#xff0c;噼里啪啦敲几下键盘就把别人系统攻破了。但如果你真的做过安全测试或者…

2026/10/20 18:29:17 阅读更多 →

最新新闻

C++11类功能升级:移动语义与资源管理实战

C++11类功能升级:移动语义与资源管理实战

1. 先从一次编译报错说起&#xff1a;为什么需要这些“新功能”大概两年前&#xff0c;我在某公司接手一个老项目&#xff0c;代码库基于C98标准&#xff0c;其中一个核心类维护着手写的内存缓冲区。当时新增需求要支持从另一个对象“转移”资源&#xff0c;我按C98的老思路写了…

2026/10/20 19:09:07 阅读更多 →
C++嵌入Python调用YOLO:从环境配置到部署的完整指南

C++嵌入Python调用YOLO:从环境配置到部署的完整指南

1. 为什么要在C里调用Python侧的YOLO模型先把场景说清楚。你手头有一个已经训练好的YOLO目标检测模型&#xff0c;可能是.pt、.onnx或者某个导出后的权重文件&#xff0c;训练和验证阶段都在Python里跑得挺顺。但真正要落地的时候&#xff0c;宿主程序是C写的——可能是一个桌面…

2026/10/20 19:09:07 阅读更多 →
结合执行器动力学的运载火箭IMU故障检测仿真

结合执行器动力学的运载火箭IMU故障检测仿真

先说结论&#xff1a;这套仿真验证了在运载火箭俯仰控制回路里&#xff0c;把执行器动力学纳入参考模型&#xff0c;能让基于IMU观测残差的故障检测在机动段依然保持稳定阈值&#xff0c;明显降低动态误报率。我按标题思路在Matlab里搭了一套桌面级仿真环境&#xff0c;参数全部…

2026/10/20 19:09:07 阅读更多 →
蓝桥杯高精度算法模板详解:大整数四则运算与压位技巧

蓝桥杯高精度算法模板详解:大整数四则运算与压位技巧

点开赛题列表&#xff0c;看到“结果可能很大&#xff0c;请使用高精度”、“输出不超过 XXX 位的整数”、“答案需要对 10^97 取模”这些字眼&#xff0c;心里就要有数了&#xff1a;高精度这关&#xff0c;蓝桥杯一定会考。不管你是 C/C 组还是 Java/Python 组&#xff0c;大…

2026/10/20 19:09:07 阅读更多 →
鸿蒙ArkUI实现单格单字符输入框:验证码/手机号优雅方案

鸿蒙ArkUI实现单格单字符输入框:验证码/手机号优雅方案

1. 项目概述1.1 为什么需要「单格单字符」输入框做鸿蒙应用开发时&#xff0c;手机号、验证码这类输入场景是最常见的需求之一。但很多新手甚至有一定经验的开发者&#xff0c;第一反应都是直接用TextInput配合maxLength限制长度&#xff0c;再套一层样式就完事。这种方案在功能…

2026/10/20 19:09:07 阅读更多 →
企业知识图谱+AI智能体:破解研发全流程工具孤岛

企业知识图谱+AI智能体:破解研发全流程工具孤岛

1. 从"工具孤岛"到"知识流动"&#xff1a;这次合作到底在解决什么问题如果你在软件研发团队待过一段时间&#xff0c;大概率见过这样的场景&#xff1a;需求写在文档系统里&#xff0c;任务追踪在项目管理工具中&#xff0c;代码托管在另一个平台&#xff…

2026/10/20 19:08:06 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →