初始导入: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
这个提交包含在:
zhou yang
2026-09-06 18:18:14 +08:00
共同撰写人 Claude Opus 5
当前提交 2bfe308b30
共修改 526 个文件,包含 80633 行新增和 0 行删除
+231
查看文件
@@ -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
})
}
}
})
+289
查看文件
@@ -0,0 +1,289 @@
/* PhoneRail 需求文档站 —— 主题定制
取色:品牌青(板子的阻焊)+ 警示琥珀(铜箔)。
两处非默认的排版决定:
1) 正文加宽到 1080px —— 文档里 5~6 列的需求表在 688px 下会被压成一堆折行;
2) ⚠️ / ✅ 开头的引用块渲染成告警卡片(转换在 config.mts 的 markdown 插件里)。 */
:root {
--vp-c-brand-1: #0d8074;
--vp-c-brand-2: #0f766e;
--vp-c-brand-3: #115e59;
--vp-c-brand-soft: rgba(13, 148, 136, 0.14);
--vp-layout-max-width: 1560px;
--vp-sidebar-width: 300px;
--rail-warn: #b45309;
--rail-warn-bg: rgba(245, 158, 11, 0.09);
--rail-warn-border: #f59e0b;
--rail-ok: #15803d;
--rail-ok-bg: rgba(34, 197, 94, 0.09);
--rail-ok-border: #22c55e;
--vp-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC',
sans-serif;
}
.dark {
--vp-c-brand-1: #2dd4bf;
--vp-c-brand-2: #14b8a6;
--vp-c-brand-3: #0d9488;
--vp-c-brand-soft: rgba(45, 212, 191, 0.16);
--rail-warn: #fbbf24;
--rail-warn-bg: rgba(245, 158, 11, 0.12);
--rail-warn-border: #b45309;
--rail-ok: #4ade80;
--rail-ok-bg: rgba(34, 197, 94, 0.11);
--rail-ok-border: #15803d;
}
/* ---------- 版心 ---------- */
.VPDoc.has-aside .content-container {
max-width: 1080px;
}
.VPDoc:not(.has-sidebar) .content {
max-width: 1080px;
}
/* ---------- 表格:这份文档主要就是表 ---------- */
.vp-doc table {
display: block;
width: max-content;
min-width: 100%;
max-width: 100%;
overflow-x: auto;
font-size: 14px;
line-height: 1.6;
border-collapse: collapse;
}
.vp-doc th {
background: var(--vp-c-bg-soft);
font-size: 13px;
letter-spacing: 0.02em;
white-space: nowrap;
}
.vp-doc th,
.vp-doc td {
padding: 7px 12px;
vertical-align: top;
}
/* 需求编号(表格首列里的 **FR-1**)等宽显示,便于扫读与引用 */
.vp-doc table td:first-child > strong {
font-family: var(--vp-font-family-mono);
font-size: 0.92em;
font-weight: 600;
color: var(--vp-c-brand-1);
white-space: nowrap;
}
/* 首列是编号 / 短标签,别让它折行 */
.vp-doc table td:first-child {
white-space: nowrap;
}
/* …但纯文字的首列(术语表、风险表)允许折 */
.vp-doc table td:first-child:not(:has(> strong)) {
white-space: normal;
}
/* ---------- 告警卡片(config.mts 里给 blockquote 打的 class) ---------- */
.vp-doc blockquote.callout {
margin: 20px 0;
padding: 14px 18px;
border-radius: 8px;
border-left-width: 4px;
border-left-style: solid;
}
.vp-doc blockquote.callout > p {
margin: 0;
color: var(--vp-c-text-1);
}
.vp-doc blockquote.callout > p + p {
margin-top: 10px;
}
.vp-doc blockquote.callout-warn {
background: var(--rail-warn-bg);
border-left-color: var(--rail-warn-border);
}
.vp-doc blockquote.callout-warn strong {
color: var(--rail-warn);
}
.vp-doc blockquote.callout-ok {
background: var(--rail-ok-bg);
border-left-color: var(--rail-ok-border);
}
.vp-doc blockquote.callout-ok strong {
color: var(--rail-ok);
}
/* ---------- 矢量框图(config.mts 把 ```diagram=xxx 换成内联 SVG) ---------- */
:root {
--dg-panel-fill: #f4f7f7;
--dg-panel-line: #ccd8d5;
--dg-box-fill: #ffffff;
--dg-box-line: #9aaeaa;
--dg-line: #7d918c;
--dg-key-fill: #e3f4f1;
--dg-key-line: #0d8074;
--dg-hot-fill: #fdf3e3;
--dg-hot-line: #d99b3a;
--dg-hard-fill: #f0f5ea;
--dg-hard-line: #7fa05e;
}
.dark {
--dg-panel-fill: #1a2220;
--dg-panel-line: #33413d;
--dg-box-fill: #222d2b;
--dg-box-line: #4d5f5a;
--dg-line: #8aa19b;
--dg-key-fill: #113029;
--dg-key-line: #2dd4bf;
--dg-hot-fill: #302516;
--dg-hot-line: #b57f2e;
--dg-hard-fill: #1e2a1a;
--dg-hard-line: #6f8d52;
}
.vp-doc figure.dg {
margin: 24px 0;
padding: 0;
overflow-x: auto;
}
/* 太窄的屏幕上让图横向滚动而不是把字压成一团 */
.vp-doc figure.dg svg {
display: block;
width: 100%;
min-width: 560px;
height: auto;
}
.vp-doc figure.dg .dg-panel {
fill: var(--dg-panel-fill);
stroke: var(--dg-panel-line);
stroke-width: 1.2;
}
.vp-doc figure.dg .dg-box {
fill: var(--dg-box-fill);
stroke: var(--dg-box-line);
stroke-width: 1.2;
}
.vp-doc figure.dg .dg-key {
fill: var(--dg-key-fill);
stroke: var(--dg-key-line);
stroke-width: 1.6;
}
.vp-doc figure.dg .dg-hot {
fill: var(--dg-hot-fill);
stroke: var(--dg-hot-line);
stroke-width: 1.4;
}
.vp-doc figure.dg .dg-hard {
fill: var(--dg-hard-fill);
stroke: var(--dg-hard-line);
stroke-width: 1.4;
}
.vp-doc figure.dg .dg-wire {
stroke: var(--dg-line);
stroke-width: 1.4;
fill: none;
}
.vp-doc figure.dg .dg-dash {
stroke-dasharray: 4 3;
}
.vp-doc figure.dg .dg-bus {
stroke: var(--dg-key-line);
stroke-width: 1.4;
fill: none;
}
.vp-doc figure.dg .dg-pins path {
stroke: var(--dg-box-line);
stroke-width: 2.2;
fill: none;
}
.vp-doc figure.dg .dg-head {
fill: var(--dg-line);
stroke: none;
}
.vp-doc figure.dg text {
font-family: var(--vp-font-family-base);
fill: var(--vp-c-text-1);
}
.vp-doc figure.dg .dg-th {
font-size: 14px;
font-weight: 700;
}
.vp-doc figure.dg .dg-t {
font-size: 13px;
}
.vp-doc figure.dg .dg-s {
font-size: 11.5px;
fill: var(--vp-c-text-2);
}
.vp-doc figure.dg .dg-c {
text-anchor: middle;
}
.vp-doc figure.dg .dg-r {
text-anchor: end;
}
/* ---------- 标题与正文细节 ---------- */
.vp-doc h2 {
margin-top: 44px;
padding-top: 22px;
border-top-color: var(--vp-c-divider);
}
.vp-doc h3 {
margin-top: 30px;
}
/* 文档里用 `---` 分节,VitePress 默认 hr 太重,压淡一点 */
.vp-doc hr {
border-top: 1px solid var(--vp-c-divider);
opacity: 0.55;
margin: 36px 0;
}
/* ASCII 框图:等宽 + 不换行,横向滚动 */
.vp-doc div[class*='language-'] pre {
line-height: 1.5;
}
/* 行内代码里的网络名 / 位号,稍微收一点 */
.vp-doc :not(pre) > code {
font-size: 0.86em;
padding: 2px 5px;
}
/* ---------- 侧边栏 ---------- */
.VPSidebarItem.level-1 > .item > .text {
font-size: 13px;
color: var(--vp-c-text-2);
}
.VPSidebarItem.level-1 .VPSidebarItem.is-link > .item > .link > .text {
font-size: 14px;
}
/* ---------- 首页顶部:文档头部那张 元信息表 更像一张卡片 ---------- */
.vp-doc h1 + table {
margin-top: 18px;
}
+7
查看文件
@@ -0,0 +1,7 @@
import DefaultTheme from 'vitepress/theme'
import type { Theme } from 'vitepress'
import './custom.css'
export default {
extends: DefaultTheme
} satisfies Theme