初始导入: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,231 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
/**
|
||||
* 侧边栏直接读**项目根**的原稿,不读 docs/ 里的副本 —— 保证目录永远跟着正文走。
|
||||
* (docs/ 里的 index.md 由 scripts/sync-docs.mjs 在 predev / prebuild 时拷进来,
|
||||
* 拷贝的原因见那个脚本顶部的注释。)
|
||||
*/
|
||||
const DOC = path.resolve(__dirname, '../../需求文档.md')
|
||||
const PLAN = path.resolve(__dirname, '../../固件开发计划.md')
|
||||
|
||||
/**
|
||||
* 与 VitePress 内部(@mdit-vue/shared)一致的 slugify。
|
||||
* 复制一份是因为 @mdit-vue/shared 打包在 vitepress 里,外部 import 不到。
|
||||
* 生成的锚点必须和正文里 <h2> 的 id 完全一致,改动前先跑 `node scripts/check-anchors.mjs`。
|
||||
*/
|
||||
function slugify(str: string): string {
|
||||
return str
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036F]/g, '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\u0000-\u001f]/g, '')
|
||||
.replace(/[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.replace(/^(\d)/, '_$1')
|
||||
}
|
||||
|
||||
const DIAGRAM_DIR = path.resolve(__dirname, '../diagrams')
|
||||
|
||||
/**
|
||||
* 把 ```diagram=<名字> 代码块换成 diagrams/<名字>.svg 的内联 SVG。
|
||||
*
|
||||
* 为什么不直接把 SVG 写进 markdown:GitHub 与编辑器会把内联 HTML 吃掉或原样显示,
|
||||
* 原稿就没法看了。所以**源文件里保留 ASCII 框图**(既是可读的降级,也是 alt 内容),
|
||||
* 只在站点构建时替换 —— 和 ⚠️ 告警块那套是同一个思路。
|
||||
*
|
||||
* ⚠️ **代价:ASCII 与 SVG 是两份。** 改了框图内容两边都要改。
|
||||
* 找不到对应 svg 时**退回渲染成普通代码块**,站点不会因此崩掉。
|
||||
*/
|
||||
function inlineDiagram(name: string): string | null {
|
||||
const f = path.join(DIAGRAM_DIR, `${name}.svg`)
|
||||
if (!fs.existsSync(f)) return null
|
||||
const svg = fs.readFileSync(f, 'utf8').replace(/<\?xml[^>]*\?>\s*/, '')
|
||||
return `<figure class="dg">${svg}</figure>`
|
||||
}
|
||||
|
||||
/** 侧边栏标签:去掉 markdown 强调、反引号,以及破折号后面的补充说明 */
|
||||
function label(text: string): string {
|
||||
return text
|
||||
.replace(/`/g, '')
|
||||
.replace(/\*\*/g, '')
|
||||
.split(/\s*——\s*/)[0]
|
||||
.trim()
|
||||
}
|
||||
|
||||
/** 从源稿里现读 h2,侧边栏永远跟正文同步 */
|
||||
function chapters(file: string, base: string): { text: string; link: string }[] {
|
||||
const out: { text: string; link: string }[] = []
|
||||
let inFence = false
|
||||
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
||||
if (/^\s*```/.test(line)) {
|
||||
inFence = !inFence
|
||||
continue
|
||||
}
|
||||
if (inFence) continue
|
||||
const m = /^##\s+(.+?)\s*$/.exec(line)
|
||||
if (!m) continue
|
||||
const raw = m[1].replace(/\s*#+\s*$/, '')
|
||||
out.push({ text: label(raw), link: `${base}#${slugify(raw)}` })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const CH = chapters(DOC, '/')
|
||||
const CH_PLAN = chapters(PLAN, '/plan')
|
||||
const pick = (prefix: string) => CH.find((c) => c.text.startsWith(prefix))!
|
||||
|
||||
export default defineConfig({
|
||||
// docs/index.md = 项目根 需求文档.md 的同步副本。需求文档本身就是首页,
|
||||
// 不另做一个空洞的 landing page。
|
||||
srcDir: 'docs',
|
||||
|
||||
lang: 'zh-CN',
|
||||
title: 'PhoneRail',
|
||||
titleTemplate: ':title · PhoneRail 项目文档',
|
||||
description: '五路手机直供电源板 —— 项目需求文档',
|
||||
cleanUrls: true,
|
||||
lastUpdated: false,
|
||||
metaChunk: true,
|
||||
|
||||
head: [
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'icon',
|
||||
href:
|
||||
'data:image/svg+xml,' +
|
||||
encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">' +
|
||||
'<rect width="32" height="32" rx="6" fill="#0f766e"/>' +
|
||||
'<path d="M18 4 9 18h6l-2 10 10-14h-6z" fill="#fbbf24"/>' +
|
||||
'</svg>'
|
||||
)
|
||||
}
|
||||
],
|
||||
['meta', { name: 'theme-color', content: '#0f766e' }]
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
siteTitle: 'PhoneRail 需求文档',
|
||||
|
||||
nav: [
|
||||
{ text: '需求文档', link: '/' },
|
||||
{ text: '固件开发计划', link: '/plan' },
|
||||
{
|
||||
text: '快速定位',
|
||||
items: [
|
||||
{ text: pick('4.').text, link: pick('4.').link },
|
||||
{ text: pick('5.').text, link: pick('5.').link },
|
||||
{ text: pick('6.').text, link: pick('6.').link },
|
||||
{ text: pick('7.').text, link: pick('7.').link },
|
||||
{ text: pick('9.').text, link: pick('9.').link },
|
||||
{ text: pick('10.').text, link: pick('10.').link },
|
||||
{ text: pick('12.').text, link: pick('12.').link }
|
||||
]
|
||||
},
|
||||
{ text: '2026-09-06', link: '/#_1-背景与目标' }
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{ text: '需求文档', items: CH, collapsed: false },
|
||||
{ text: '固件开发计划', items: CH_PLAN, collapsed: true }
|
||||
],
|
||||
|
||||
outline: { level: [2, 3], label: '本页目录' },
|
||||
aside: true,
|
||||
|
||||
docFooter: { prev: false, next: false },
|
||||
returnToTopLabel: '回到顶部',
|
||||
sidebarMenuLabel: '目录',
|
||||
darkModeSwitchLabel: '主题',
|
||||
lightModeSwitchTitle: '切换到浅色',
|
||||
darkModeSwitchTitle: '切换到深色',
|
||||
|
||||
search: {
|
||||
provider: 'local',
|
||||
options: {
|
||||
translations: {
|
||||
button: { buttonText: '搜索', buttonAriaLabel: '搜索文档' },
|
||||
modal: {
|
||||
displayDetails: '显示详情',
|
||||
resetButtonTitle: '清除',
|
||||
backButtonTitle: '返回',
|
||||
noResultsText: '没有找到',
|
||||
footer: {
|
||||
selectText: '选择',
|
||||
navigateText: '切换',
|
||||
closeText: '关闭'
|
||||
}
|
||||
}
|
||||
},
|
||||
miniSearch: {
|
||||
// 默认分词按非单词字符切,中文会整段变成一个 token,搜不到。
|
||||
// 用 Intl.Segmenter 按词切一遍,再退回默认切分处理英文 / 型号。
|
||||
options: {
|
||||
tokenize: (text: string) => {
|
||||
const seg = new Intl.Segmenter('zh-CN', { granularity: 'word' })
|
||||
const out: string[] = []
|
||||
for (const { segment, isWordLike } of seg.segment(text)) {
|
||||
if (isWordLike) out.push(segment)
|
||||
}
|
||||
return out
|
||||
},
|
||||
processTerm: (term: string) => term.toLowerCase()
|
||||
},
|
||||
searchOptions: { fuzzy: 0.2, prefix: true, boost: { title: 4, text: 2 } }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
footer: {
|
||||
message:
|
||||
'本文只回答「要做成什么样」;「怎么做到」在 <code>五路直供电源板设计.md</code> 里。',
|
||||
copyright: 'PhoneRail · 五路手机直供电源板'
|
||||
}
|
||||
},
|
||||
|
||||
markdown: {
|
||||
lineNumbers: false,
|
||||
theme: { light: 'github-light', dark: 'github-dark' },
|
||||
config(md) {
|
||||
// ```diagram=xxx → 内联 diagrams/xxx.svg(找不到就照常渲染成代码块)
|
||||
const fence = md.renderer.rules.fence!
|
||||
md.renderer.rules.fence = (tokens, idx, opts, env, self) => {
|
||||
const m = /^diagram=([a-z0-9-]+)$/i.exec(tokens[idx].info.trim())
|
||||
if (m) {
|
||||
const svg = inlineDiagram(m[1])
|
||||
if (svg) return svg
|
||||
console.warn(`[diagram] 找不到 diagrams/${m[1]}.svg,退回代码块`)
|
||||
}
|
||||
return fence(tokens, idx, opts, env, self)
|
||||
}
|
||||
|
||||
// 正文里以 ⚠️ / ❌ 开头的引用块渲染成警告卡片,以 ✅ 开头的渲染成通过卡片。
|
||||
// 用插件而不是 ::: warning,是为了让 markdown 源文件在 GitHub / 编辑器里也正常。
|
||||
md.core.ruler.push('callout_blockquote', (state) => {
|
||||
const t = state.tokens
|
||||
for (let i = 0; i < t.length; i++) {
|
||||
if (t[i].type !== 'blockquote_open') continue
|
||||
let text = ''
|
||||
for (let j = i + 1; j < t.length; j++) {
|
||||
if (t[j].type === 'blockquote_close') break
|
||||
if (t[j].type === 'inline') {
|
||||
text = t[j].content
|
||||
break
|
||||
}
|
||||
}
|
||||
if (/^\s*[⚠❗❌]/.test(text)) t[i].attrJoin('class', 'callout callout-warn')
|
||||
else if (/^\s*✅/.test(text)) t[i].attrJoin('class', 'callout callout-ok')
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
在新工单中引用
屏蔽一个用户