初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。 这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。 纳入: - 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本 - .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、 pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源) - 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾) - firmware/ 的 A1 骨架与 site/ 文档站源码 .gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、 site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。 .gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存, 不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
这个提交包含在:
@@ -0,0 +1,85 @@
|
||||
// 校验侧边栏 / 导航里生成的锚点,在构建出来的 HTML 里真的存在。
|
||||
//
|
||||
// 侧边栏是 config.mts 用一份**自己实现的** slugify 从源稿的 h2 现算出来的
|
||||
// (@mdit-vue/shared 打包在 vitepress 内部,外面 import 不到)。
|
||||
// 只要那份实现和 VitePress 的哪怕差一个字符,链接就会静默跳空 —— 页面照样打开,
|
||||
// 只是不滚动,肉眼极难发现。所以每次 build 之后自动比一遍。
|
||||
//
|
||||
// 多页之后还多了一类错:锚点对了但**页面指错了**(/#xxx 写成 /plan#xxx)。
|
||||
// 所以这里按「路由 → 该页的 id 集合」建表,逐条链接校验它落在正确的页上。
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const DIST = path.resolve(here, '../.vitepress/dist')
|
||||
|
||||
if (!fs.existsSync(DIST)) {
|
||||
console.error(`[check-anchors] 没有构建产物:${DIST}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// VitePress 自己注入的锚点,不是正文标题
|
||||
const BUILTIN = new Set(['VPContent', 'VPSidebarNav'])
|
||||
|
||||
/** dist 下的全部 html(不含 404) */
|
||||
function pages(dir, out = []) {
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, e.name)
|
||||
if (e.isDirectory()) pages(p, out)
|
||||
else if (e.name.endsWith('.html') && e.name !== '404.html') out.push(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 文件路径 → cleanUrls 下的路由:index.html -> '/',plan.html -> '/plan' */
|
||||
function routeOf(file) {
|
||||
const rel = path.relative(DIST, file).split(path.sep).join('/').replace(/\.html$/, '')
|
||||
return rel === 'index' ? '/' : '/' + rel.replace(/\/index$/, '')
|
||||
}
|
||||
|
||||
const idsByRoute = new Map()
|
||||
const files = pages(DIST)
|
||||
for (const f of files) {
|
||||
const html = fs.readFileSync(f, 'utf8')
|
||||
const ids = new Set([...html.matchAll(/<h[1-6][^>]*\sid="([^"]+)"/g)].map((m) => m[1]))
|
||||
idsByRoute.set(routeOf(f), ids)
|
||||
}
|
||||
|
||||
let checked = 0
|
||||
const bad = []
|
||||
|
||||
for (const f of files) {
|
||||
const from = routeOf(f)
|
||||
const html = fs.readFileSync(f, 'utf8')
|
||||
for (const m of html.matchAll(/href="([^"]*#[^"]+)"/g)) {
|
||||
const href = m[1]
|
||||
const hash = decodeURIComponent(href.slice(href.indexOf('#') + 1))
|
||||
if (BUILTIN.has(hash)) continue
|
||||
const rawPath = href.slice(0, href.indexOf('#'))
|
||||
if (rawPath && !rawPath.startsWith('/')) continue // 外链
|
||||
const route = rawPath === '' ? from : rawPath === '/' ? '/' : rawPath.replace(/\/$/, '')
|
||||
checked++
|
||||
const ids = idsByRoute.get(route)
|
||||
if (!ids) bad.push(`${from}:链接指向不存在的页面 ${route}`)
|
||||
else if (!ids.has(hash)) bad.push(`${from}:${route}#${hash} —— 该页没有这个标题`)
|
||||
}
|
||||
}
|
||||
|
||||
if (bad.length) {
|
||||
console.error(`\n[check-anchors] ✗ ${bad.length} 条锚点对不上:`)
|
||||
for (const b of [...new Set(bad)]) console.error(` ${b}`)
|
||||
console.error(`\n 多半是 config.mts 里的 slugify() 与 VitePress 不一致,或侧边栏的 base 写错了页。`)
|
||||
for (const [route, ids] of idsByRoute) {
|
||||
console.error(` ${route} 的标题 id:`)
|
||||
for (const id of ids) console.error(` #${id}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const total = [...idsByRoute.values()].reduce((n, s) => n + s.size, 0)
|
||||
console.log(
|
||||
`[check-anchors] ✓ ${checked} 条锚点全部对得上` +
|
||||
`(${idsByRoute.size} 个页面,共 ${total} 个标题)`
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
// 把项目根的 markdown 同步进 site/docs/,由 npm 的 predev / prebuild 自动跑。
|
||||
//
|
||||
// 为什么要拷而不是让 VitePress 直接指向项目根:
|
||||
// srcDir 指到 D:\DCDC 之后,那里的 .md 编译出的虚拟模块要 import 'vue/server-renderer',
|
||||
// 而 node_modules 在 site/ 下(是子目录不是父目录),Rollup 解析不到,构建直接失败。
|
||||
// 拷进来是最省事且不污染硬件工程根目录的做法。
|
||||
//
|
||||
// ⚠️ site/docs/ 下的文件是**生成物**,别在里面改内容 —— 改 D:\DCDC\需求文档.md。
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = path.resolve(here, '../..')
|
||||
const OUT = path.resolve(here, '../docs')
|
||||
|
||||
/** [源文件, 站点内路径] */
|
||||
const PAGES = [
|
||||
['需求文档.md', 'index.md'],
|
||||
['固件开发计划.md', 'plan.md']
|
||||
]
|
||||
|
||||
fs.mkdirSync(OUT, { recursive: true })
|
||||
|
||||
for (const [src, dst] of PAGES) {
|
||||
const from = path.join(ROOT, src)
|
||||
const to = path.join(OUT, dst)
|
||||
if (!fs.existsSync(from)) {
|
||||
console.error(`[sync-docs] 找不到源文件:${from}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const body = fs.readFileSync(from, 'utf8')
|
||||
const prev = fs.existsSync(to) ? fs.readFileSync(to, 'utf8') : null
|
||||
if (prev === body) {
|
||||
console.log(`[sync-docs] 未变化 ${src}`)
|
||||
continue
|
||||
}
|
||||
fs.writeFileSync(to, body)
|
||||
console.log(`[sync-docs] 已同步 ${src} -> docs/${dst} (${body.length} 字符)`)
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户