Vue3 + Vite 实战:接入钉钉 OAuth 扫码登录(内嵌二维码 + 跳转授权)
Vue3 Vite 实战接入钉钉 OAuth 扫码登录内嵌二维码 跳转授权本文基于 Vue 3 Vite TypeScript Pinia 的登录页工程完整演示钉钉开放平台OAuth2 授权码模式内嵌扫码DTFrameLogin与整页跳转授权两条链路并说明前后端如何用code换取业务 Token。照着步骤做本地即可跑通。一、先搞清楚我们要接的是哪一种「钉钉登录」钉钉开放能力里常见两类登录容易混类型典型场景前端关键字段本文是否覆盖OAuth2 网站应用登录PC 网页扫码 / 跳转授权拿code换用户身份client_id、redirect_uri、scopeopenid是企业内部 H5 / JSAPI钉钉客户端内打开 H5用corpId、dd.readycorpId、AgentId 等否本文方案是用户打开登录页 → 扫码或跳转钉钉授权 → 前端拿到授权码code→ 交给自家后端 → 后端用 AppSecret 向钉钉换用户信息并签发业务 Token → 前端进入系统首页。要点一句话前端只持有 Client IDAppKey可以写进环境变量。AppSecret / Client Secret 只能放在服务端绝不能出现在前端仓库或浏览器包里。二、整体架构与登录时序┌─────────────┐ 加载 CDN SDK ┌──────────────────────┐ │ 登录页 │ ───────────────────▶ │ g.alicdn.com │ │ (Vue SPA) │ │ h5-dingtalk-login │ └──────┬──────┘ └──────────────────────┘ │ │ ① DTFrameLogin 内嵌二维码 │ 或 ② 跳转 login.dingtalk.com/oauth2/auth ▼ ┌──────────────────────┐ │ 钉钉授权页 / 扫码端 │ └──────────┬───────────┘ │ 返回 authCode / ?code ▼ ┌──────────────────────┐ POST { code } ┌─────────────────┐ │ handleLoginByCode │ ──────────────────▶ │ 业务后端 │ └──────────────────────┘ │ /api/login/ │ │ dingtalk │ └────────┬────────┘ │ 用 Secret 调钉钉 API │ 签发 accessToken ▼ 前端存 Token跳转系统首页两条前端入口最终汇合到同一接口内嵌扫码SDK 成功回调里直接拿到authCode。按钮跳转钉钉把用户重定向回redirect_uri?codexxxstateyyy登录页从 URL 读取code。三、开放平台侧准备可实操清单3.1 创建应用打开 钉钉开放平台登录开发者账号。创建企业内部应用或按文档创建具备「登录」能力的应用以控制台当前产品名为准。在应用详情中找到Client ID也常叫 AppKey—— 给前端用。Client Secret也常叫 AppSecret——只给后端用。3.2 配置回调地址最容易踩坑在「登录与分享」或「应用首页 / 回调域名」一类配置里把授权回调地址加入白名单。地址必须与代码里拼出来的redirect_uri完全一致含协议、域名、路径、查询串。示例请换成你自己的域名https://www.example.com/login?typeding本地调试时若走内嵌扫码且redirect_uri取当前页面源还需要额外加http://localhost:8007/login?typeding经验跳转授权路径若写死了生产域名本地点「钉钉登录」按钮会跳到生产环境而不是本机。内嵌二维码一般用window.location.origin两边要分开想清楚。3.3 权限与 scope网站扫码登录常用response_typecodescopeopenidpromptconsent首次或需要用户确认授权时后端换 Token、查用户信息所需的接口权限在开放平台按官方文档开通具体接口名以钉钉最新文档为准。四、前端工程准备4.1 技术栈约定本文示例栈Vue 3 Vue Router 4 PiniaVite 5 TypeScriptAxios钉钉登录 SDKCDN 引入不装 npm 包CDN 地址https://g.alicdn.com/dingding/h5-dingtalk-login/0.37.0/ddlogin.js加载成功后全局会挂上window.DTFrameLogin部分旧文档还会提到DDLogin本方案以DTFrameLogin为准。4.2 环境变量在项目根目录.env/.env.development/.env.production中配置# 钉钉 OAuth Client ID与开放平台应用一致VITE_DINGTALK_CLIENT_IDdingxxxxxxxxxxxxxxxxVITE_前缀才会被 Vite 注入到前端代码。types/global.d.ts里可为ImportMetaEnv补上类型interfaceImportMetaEnv{readonlyVITE_DINGTALK_CLIENT_ID?:string;// ...}4.3 TypeScript 声明 SDK新建types/dingtalk.d.tsdeclareglobal{interfaceWindow{DTFrameLogin?:(config:{id:string;width:number;height:number},authConfig:{redirect_uri:string;client_id:string;scope?:string;response_type?:string;state?:string;prompt?:string;},onSuccess:(result:{redirectUrl?:string;authCode?:string;state?:string;})void,onFail?:(error:string)void)void;}}export{};五、工具层加载 SDK、拼跳转 URL、生成 state建议单独建src/utils/dingtalkAuth.ts把「可配置项」集中管理。/** 整页跳转授权使用的回调地址须与开放平台白名单一致 */constREDIRECT_URIhttps://www.example.com/login?typeding;exportfunctiongetClientId():string{constidimport.meta.env.VITE_DINGTALK_CLIENT_IDasstring|undefined;return(idString(id).trim())||;}/** CSRF 防护用的 state */exportconstgenerateState(){if(window?.crypto?.randomUUID){returnwindow.crypto.randomUUID();}returnstate-Date.now();};/** 内嵌扫码按当前访问源动态生成 redirect_uri需 URL encode */exportconstgetEncodedRedirectUri(){if(window?.location){returnencodeURIComponent(window.location.origin/login?typeding);}returnencodeURIComponent(REDIRECT_URI);};/** 动态注入钉钉登录 SDK只加载一次 */exportconstloadLoginSdk(version0.37.0){returnnewPromisevoid((resolve,reject){if(window.DTFrameLogin){resolve();return;}constscriptdocument.createElement(script);script.srchttps://g.alicdn.com/dingding/h5-dingtalk-login/${version}/ddlogin.js;script.onload()resolve();script.onerror()reject(newError(钉钉SDK加载失败));document.head.appendChild(script);});};/** 整页跳转到钉钉授权页 */exportconstredirectToAuthPage(){constclientIdgetClientId();constredirectUriREDIRECT_URI;conststategenerateState();sessionStorage.setItem(dingtalk_login_state,state);consturlnewURL(https://login.dingtalk.com/oauth2/auth);url.searchParams.set(redirect_uri,redirectUri);url.searchParams.set(response_type,code);url.searchParams.set(client_id,clientId);url.searchParams.set(scope,openid);url.searchParams.set(prompt,consent);url.searchParams.set(state,state);window.location.hrefurl.toString();};说明generateStatesessionStorage用于防 CSRF回调落地后建议校验state是否与本地一致见后文「踩坑」。内嵌扫码与按钮跳转的redirect_uri可以不同策略一个跟当前域名一个跟生产域名。两边都必须在开放平台登记。可在App.vue的onMounted里提前loadLoginSdk()缩短用户打开登录页后的等待。六、UI 组件内嵌二维码 「钉钉登录」按钮组件职责挂载后加载 SDK调用DTFrameLogin渲染二维码。扫码成功 →emit(login, authCode)。点击按钮 →redirectToAuthPage()整页授权。失败展示错误文案与重试。核心逻辑示意src/components/QrLoginPanel/index.vuetemplate div classflex flex-col justify-center items-center w-full h-full div classdd-qr-wrap div iddingtalk-container classdd-qr-inner/div div v-ifisLoading classdd-login-overlay n-spin sizesmall description加载钉钉登录... / /div /div n-text v-iferrorMessage typeerror{{ errorMessage }}/n-text n-button v-iferrorMessage quaternary clickhandleRetry重试/n-button n-button typeprimary clickhandleAuthRedirect钉钉登录/n-button /div /template script langts import { ref, defineComponent, onMounted } from vue; import { loadLoginSdk, getClientId, generateState, redirectToAuthPage, getEncodedRedirectUri, } from /utils/dingtalkAuth; export default defineComponent({ name: QrLoginPanel, emits: [login, error], setup(_, { emit }) { const isLoading ref(false); const errorMessage ref(); const onAuthSuccess (result: { authCode?: string }) { emit(login, result.authCode); }; const onAuthFail (error: unknown) { const msg typeof error string ? error : String(error); errorMessage.value msg; emit(error, msg); }; const renderQrCode () { const clientId getClientId(); const state generateState(); const redirectUri getEncodedRedirectUri(); sessionStorage.setItem(dingtalk_login_state, state); window.DTFrameLogin?.( { id: dingtalk-container, width: 300, height: 300 }, { redirect_uri: redirectUri, client_id: clientId, scope: openid, state, response_type: code, prompt: consent, }, onAuthSuccess, onAuthFail ); }; const initLogin async () { errorMessage.value ; if (!window.DTFrameLogin) { await loadLoginSdk(); } renderQrCode(); }; const handleRetry async () { isLoading.value true; try { await initLogin(); } catch (e) { onAuthFail(e); } finally { isLoading.value false; } }; const handleAuthRedirect () { try { redirectToAuthPage(); } catch (e) { onAuthFail(e); } }; onMounted(async () { isLoading.value true; try { await initLogin(); } catch (e) { onAuthFail(e); } finally { isLoading.value false; } }); return { isLoading, errorMessage, handleAuthRedirect, handleRetry }; }, }); /script容器样式要点给#dingtalk-container固定宽高如 300×300与DTFrameLogin的width/height一致避免二维码被裁切。登录页挂上组件n-tab-pane nameding tab钉钉扫码登录 QrLoginPanel loginhandleLoginByCode errorhandleScanError / /n-tab-pane七、拿到 code 之后调后端换业务 Token7.1 API 封装// src/api/user.tsimporthttpfrom/utils/http/axios;/** 钉钉扫码 / 授权回调登录 */exportfunctionloginByCode(params:{code:string;state?:string}){returnhttp.request({url:/api/login/dingtalk,method:post,data:params,},{// 保留后端原始结构自行判断 success / accessTokenisTransformResponse:false,});}请求体字段名以你们后端约定为准。本文示例发送{ code }注意若类型里曾写成authCode要以实际请求体为准避免类型与报文不一致。7.2 Pinia Store// store 片段asyncloginWithCode(params:{code:string;state?:string}){constresponseawaitloginByCode(params);const{data,success}response;if(data?.accessToken){constex7*24*60*60*1000;storage.set(ACCESS_TOKEN,data.accessToken,ex);storage.set(CURRENT_USER,data,ex);this.setToken(data.accessToken);this.setUserInfo(data);}returnresponse;}7.3 登录页统一处理扫码回调 URL 回跳consthandleLoginByCodeasync(authCode:string|any){if(!authCode||typeofauthCode!string){message.warning(未获取到授权码请重试);return;}// 建议同时校验 state见第八节constpayload{code:authCode};try{constresawaituserStore.loginWithCode(payload);const{success,message:msg,data}resas{success?:boolean;message?:string;data?:{accessToken?:string;account?:{id?:string;personName?:string;username?:string};};};if(!success||!data?.accessToken){message.error(msg||登录失败);return;}message.success(登录成功即将进入系统);router.replace(/);}catch(e:unknown){message.error(einstanceofError?e.message:登录失败);}};consthandleScanError(msg:string){message.error(msg||钉钉登录异常);};onMounted((){consturlParamsnewURLSearchParams(window.location.search);constcodeurlParams.get(code);if(code){loginType.valueding;handleLoginByCode(code);}});后端期望响应形态示例{success:true,message:ok,data:{accessToken:eyJhbGciOi...,account:{id:10001,personName:张三,username:zhangsan}}}7.4 后端要做什么前端对接视角前端仓库通常不包含 Secret 换票逻辑但联调时你需要后端同事实现大致流程接收POST /api/login/dingtalk读取code。使用Client ID Client Secret调用钉钉「用 code 换 userAccessToken / 用户信息」接口以钉钉最新 OpenAPI 为准。用钉钉用户唯一标识如unionId/openId匹配或绑定本地账号。签发你们自己的accessToken返回给前端。切记Secret 只出现在服务端配置中心或密钥库。八、本地联调步骤按顺序打勾Step 1配置环境npminstall编辑.env.developmentVITE_PORT8007VITE_DINGTALK_CLIENT_IDdingxxxxxxxxxxxxxxxx VITE_GLOB_API_URL_PREFIX/api# 开发代理指向你的后端服务示例VITE_PROXY[[/api,https://api.example.com]]Step 2开放平台白名单至少登记生产https://www.example.com/login?typeding本地若用动态 origin 扫码http://localhost:8007/login?typedingStep 3启动前端npmrun dev浏览器打开http://localhost:8007/login默认切到「钉钉扫码登录」页签应看到二维码区域。Step 4验证扫码链路手机钉钉扫码并确认授权。浏览器 Network 出现POST /api/login/dingtalkRequest Payload 含code。响应success: true且带accessToken。前端保存 Token 后跳转到系统首页如/。Step 5验证跳转链路点击「钉钉登录」。跳转到https://login.dingtalk.com/oauth2/auth?...授权后回到配置的redirect_uri地址栏出现code。登录页onMounted读到code后自动走同一套换票逻辑。九、常见问题与踩坑1. 二维码空白 / SDK 加载失败检查 CDN 是否被公司网络拦截可在 Network 看ddlogin.js是否 200。确认#dingtalk-container在调用DTFrameLogin时已挂载到 DOM。提供「重试」按钮重新执行initLogin。2.redirect_uri不匹配钉钉会直接拒绝授权。核对协议http/https端口本地8007路径/login查询参数?typeding是否也写进了白名单若代码里带了查询串白名单一般也要带3. 本地扫码能用按钮跳转却去了生产站这是「动态 origin」与「写死生产回调」两套策略并存时的正常现象。开发阶段可把redirectToAuthPage的redirectUri也改成当前 origin或单独做环境分支。4. 前端发了code后端却说字段不对对齐字段名codevsauthCode。以实际 JSON 为准不要只信类型定义。5.state写了却没校验写入sessionStorage[dingtalk_login_state]后回调时应conststateFromUrlurlParams.get(state);conststateLocalsessionStorage.getItem(dingtalk_login_state);if(stateFromUrlstateLocalstateFromUrl!stateLocal){message.error(登录状态校验失败请重试);return;}内嵌扫码成功回调里也会带回state同样建议比对。6. 登录成功但不跳转换票成功后记得显式跳转如router.replace(/)。若只存了 Token 却没有路由跳转用户会感觉「卡住」。7. Client ID 写进前端是否安全Client ID 本身是公开标识会出现在授权 URL 和前端包中这是 OAuth 公开客户端的常态。真正敏感的是Secret以及后端签发的业务 Token。十、文件清单对照实现路径作用.env*VITE_DINGTALK_CLIENT_IDtypes/dingtalk.d.tsDTFrameLogin全局类型src/utils/dingtalkAuth.tsSDK 加载、Client ID、跳转授权、statesrc/components/QrLoginPanel/index.vue内嵌二维码 跳转按钮src/views/login/index.vue处理授权码换票并进入首页src/api/user.tsPOST /api/login/dingtalksrc/store/modules/user.tsloginWithCode持久化 Tokensrc/App.vue可选预加载 SDK十一、小结接入钉钉网页扫码登录可以按这条最短路径落地开放平台创建应用拿到 Client ID / Secret配齐回调白名单。前端 CDN 加载h5-dingtalk-login用DTFrameLogin做内嵌扫码必要时再做oauth2/auth整页跳转。两条路都只负责拿到授权码用 Secret 换用户身份、发业务 Token 必须在服务端完成。登录成功后保存 Token并跳转到系统首页。把回调地址、字段名、state校验这三处对齐联调成功率会高很多。其余 UI、Tab、加载态按你们设计系统微调即可。参考链接钉钉开放平台钉钉登录 JS SDKCDNhttps://g.alicdn.com/dingding/h5-dingtalk-login/OAuth 授权入口https://login.dingtalk.com/oauth2/auth具体换票、用户信息接口以开放平台当前文档版本为准接口路径偶有迭代联调时请对照最新文档。

相关新闻

自动化文档命名系统:解决无标题文件管理难题

自动化文档命名系统:解决无标题文件管理难题

1. 项目概述作为一名从业多年的技术博主,我经常遇到一个令人头疼的问题——那些随手保存的代码片段、临时记录的想法、未完成的草稿,总是以"无标题"的形式散落在电脑各处。这个看似简单的命名问题,实际上反映了数字时代知识管理的普…

2026/10/7 10:21:13 阅读更多 →
LangChain与LangGraph:大模型应用开发中的链与图架构解析

LangChain与LangGraph:大模型应用开发中的链与图架构解析

如果你刚开始接触大语言模型应用开发,大概率会同时遇到 LangChain 和 LangGraph 这两个名字。它们都带着“Lang”前缀,都出现在各种教程和项目里,甚至很多文章会把它们混在一起讲。这很容易让人困惑:它们到底是一个东西的两个部分…

2026/10/7 3:11:40 阅读更多 →
Java转大模型:会调API的人很多,能搞定权限日志的为什么少?

Java转大模型:会调API的人很多,能搞定权限日志的为什么少?

聊《Java转大模型,真正值钱的为什么不是会调 API?》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。摘要最近面试了几个从Java后端转大模型的同学,有个现象挺有意思:简…

2026/10/14 13:41:05 阅读更多 →

最新新闻

图是一篇论文的门面,科迅捷AI帮你画到审稿人挑不出毛病

图是一篇论文的门面,科迅捷AI帮你画到审稿人挑不出毛病

博士论文里,图不只是插图,它是论证的一部分,是论文的门面。审稿人翻开论文,第一眼扫过的往往是图表。图做得好,读者对整篇论文的第一印象就好。这篇聊聊怎么把图画到挑不出毛病。 第一,图必须自洽。一张合…

2026/10/15 19:25:19 阅读更多 →
中牟全屋定制怎么选才不踩坑

中牟全屋定制怎么选才不踩坑

在中牟装修新房,很多业主都会纠结一个问题:全屋定制到底值不值得做?要回答中牟全屋定制怎么选不被坑,首先得把全屋定制优缺点看清楚。全屋定制的核心优势在于整体性强。以中牟欣琪雅全屋定制工厂的门墙柜一体化全屋定制为例&#…

2026/10/15 19:25:19 阅读更多 →
React Native iOS图片不显示?ATS、Base64与本地资源排查手册

React Native iOS图片不显示?ATS、Base64与本地资源排查手册

React Native 在 iOS 上跑起来,功能逻辑都没问题,结果一进详情页图片全空白;Android 上好好的,iOS 就是显示不出来。这种问题我处理过很多次,大部分情况不是 React Native 的 bug,而是 iOS 平台固有的机制在…

2026/10/15 19:25:19 阅读更多 →
9款AI写论文哪个好?云智变AI 云智变AI官网www.yunzhibian.cn 微信公众号搜一搜 云智变ai学术——一篇帮你从“比功能”升级到“比骨架”的选购指南

9款AI写论文哪个好?云智变AI 云智变AI官网www.yunzhibian.cn 微信公众号搜一搜 云智变ai学术——一篇帮你从“比功能”升级到“比骨架”的选购指南

先说一个反直觉的发现:大多数人在搜“9款AI写论文哪个好”的时候,真正应该问的问题不是“哪个软件功能多”,而是“哪个软件能帮我把论证搭结实”。 为什么?因为2026年高校对AI工具的使用规范已经收紧了。中国学位与研究生教育学会…

2026/10/15 19:25:19 阅读更多 →
第14章:恶意代码防范技术原理与应用

第14章:恶意代码防范技术原理与应用

震网病毒攻击的是西门子公司 SIMATIC 的 WinCC病毒命名前缀Worm网络蠕虫(WannaCry 永恒之蓝)Trojan特洛伊木马(植入威胁)Script脚本病毒(Script.Redlof)Harm破坏程序病毒Binder捆绑机病毒Macro宏病毒文件型…

2026/10/15 19:25:19 阅读更多 →
30天从零开始学AI应用开发(Day 26):项目三完工:调试 Agent 的血泪经验

30天从零开始学AI应用开发(Day 26):项目三完工:调试 Agent 的血泪经验

这是系列的第 26 篇。整个系列写给零基础、想入行 AI 的朋友,每天一篇,30 天后你会做出 3 个能写进简历的项目。这篇解决什么问题 昨天项目三跑通了,有几个朋友说规划步骤挺像样,也有人报告了各种奇怪的状况:跑着跑着不…

2026/10/15 19:24:19 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →