This commit is contained in:
oneao committed 2026-10-12 22:25:41 +08:00
1 parent ae3ca3cfa1
commit 7bd5f1dde9
45 files changed
+3414 -229

No files matched your search

@@ -0,0 +1,38 @@
<script setup lang="ts">
/**
* API 参数面板 —— 给 markdown 表格套一层「带标题的框」。
*
* 为什么收 slot 而不是数组 prop:
* 仓库约定 API 表格**直接写在 md 里**(见 CODEBUDDY.md「组件文档写作约定」)。
* 若改成 `:rows="[{ name: 'view_name', ... }]"`,表格会退化成一长行 JS 字面量,
* md 源码既不好读也不好 diff。所以这里只负责外壳与版式,表格本体仍由
* markdown 渲染后塞进 slot。
*
* 配套样式见 theme/api.css(.g3-api-panel)。
*
* variant:
* params(默认)参数表 —— 列序约定「参数 / 类型 / 必填 / 说明」,第 2、3 列的样式
* 按位置写,改列序要同步改 api.css。
* bare 只给框和标题,不套列样式。用于形状不同的表(如错误码表:
* code / 含义 / 典型原因 / 定位)—— 若套上参数表的列样式,散文列
* 会被当成「类型」套等宽、被当成「必填」染成品牌色。
*
* ⚠ 刻意不叫 plain:G3ApiPayload 表示「无语气」的类名就是 .g3-api-panel--plain,
* 两者含义不同,同名迟早埋坑。
*/
withDefaults(defineProps<{ title?: string | null; variant?: 'params' | 'bare' }>(), {
title: '请求参数',
variant: 'params',
})
</script>
<template>
<div class="g3-api-panel" :class="`g3-api-panel--${variant}`">
<!-- title 传 null 时不渲染标题栏:表格已经在自己那节标题下面时,
重复一遍标题只是噪音,只保留边框与行分隔线。 -->
<div v-if="title !== null" class="g3-api-panel__head">{{ title }}</div>
<div class="g3-api-panel__body">
<slot />
</div>
</div>
</template>
@@ -0,0 +1,57 @@
<script setup lang="ts">
/**
* API 报文面板 —— 请求体 / 响应体,按 **body.code** 着色。
*
* 存在理由:原来请求体和响应体都是裸 ```json 代码块,视觉上完全一样,
* 读者分不清哪块是入参哪块是出参;而 `code: 0` 与 `code: 1000` 也同样是
* 一片灰底,全站最想强调的「HTTP 200 也可能是失败」在版式上零体现。
*
* ⚠ 着色依据是 body.code,**不是 HTTP 状态码**。这正是通用 OpenAPI 渲染器
* 做不到的地方:它们的颜色模型绑死了 HTTP 状态码,会把这里的失败响应
* 渲染成绿色成功块。
*
* tone 的推断规则:给了 code 就是「0 成功、其余失败」;语义特殊的码
* (如部分成功)显式传 tone="warn" 覆盖。
*/
import { computed } from 'vue'
type Tone = 'plain' | 'ok' | 'warn' | 'error'
const props = withDefaults(
defineProps<{
/** 面板标题,如「请求体」「响应」 */
title?: string
/** 响应体里的 code;给出时参与 tone 推断,并在标题栏显示 code 徽章 */
code?: number | string
/** 显式指定语气,覆盖由 code 推断的结果 */
tone?: Tone
}>(),
{ title: '响应' },
)
const resolvedTone = computed<Tone>(() => {
if (props.tone) return props.tone
if (props.code === undefined) return 'plain'
return props.code === 0 ? 'ok' : 'error'
})
/** 颜色之外再给一个文字标签:色觉障碍下也能读出成功/失败 */
const toneLabel = computed(() => {
const map: Record<Tone, string> = { plain: '', ok: '成功', warn: '警告', error: '失败' }
return map[resolvedTone.value]
})
</script>
<template>
<div class="g3-api-panel g3-api-panel--payload" :class="`g3-api-panel--${resolvedTone}`">
<div class="g3-api-panel__head">
<span class="g3-api-panel__dot" />
<span>{{ title }}</span>
<code v-if="code !== undefined" class="g3-api-panel__code">code {{ code }}</code>
<span v-if="toneLabel" class="g3-api-panel__tone">{{ toneLabel }}</span>
</div>
<div class="g3-api-panel__body">
<slot />
</div>
</div>
</template>
@@ -0,0 +1,240 @@
/* ==========================================================================
API 文档版式(docs/content/api/**)
--------------------------------------------------------------------------
三处视觉缺口,对应下面三段:
1. 端点标题 `## `POST` `/data/page`` 与 `## 基础约定` 长得一样 → 方法徽章
2. 参数表是裸 markdown 表格(必填列原来是 emoji)→ G3ApiParams 面板
3. 请求体 / 响应体是两块一模一样的灰底 JSON → G3ApiPayload 面板(按 code 着色)
两条自我约束:
- 只用 --vp-c-* 变量,不引第三方样式表。站点其余部分是逐像素对齐 Docus 的
(见 custom.css),API 页必须留在同一套设计语言里。
- 一律逻辑属性(inline-* / block-*),站内已有 RTL 支持(见 content/ui/usage/rtl)。
========================================================================== */
/* ==========================================================================
1. 端点标题
--------------------------------------------------------------------------
写法:## `POST` `/data/page`
用 :has(> code:first-child) 把端点标题与普通 H2 区分开 —— 这既是样式的需要,
也是「为什么不用组件渲染标题」的答案:标题一旦交给 Vue 组件渲染,就不再进
markdown 的 heading token 流,右侧目录(G3DocAside)会丢掉全部条目、锚点
一并消失。所以标题仍是 markdown H2,只在 CSS 上做文章。
代价:方法徽章的颜色没法按 GET/POST 区分(CSS 不能按文本匹配)。当前
/api/** 下全部是 POST,所以是准确的;将来出现混合方法时,在 config.mts 的
markdown.config 里加一个「把首词换成 <span class="g3-api-method--get">」的
小插件即可,slug 由文本 token 决定,不受影响。
作用域:三张 API 页都带 frontmatter `pageClass: g3-api`(VitePress 把该 class
落到根 <div class="Layout"> 上,见主题 Layout.vue:34),下面全部规则都由
.g3-api 兜住,不会漏到组件页去。
========================================================================== */
.g3-api .vp-doc h2:has(> code:first-child) {
/* 覆盖 VitePress 的 .vp-doc h2(上边框 + padding-top):端点标题改为
「下方一条分隔线」,与紧随其后的参数面板构成一组,读起来是一个接口一节。 */
margin-block: 56px 20px;
padding-block: 0 14px;
border-block-start: 0;
border-block-end: 1px solid var(--vp-c-divider);
font-size: 20px;
line-height: 28px;
}
/* 方法徽章:第一个 code */
.g3-api .vp-doc h2:has(> code:first-child) > code:first-child {
padding: 2px 8px;
border-radius: 6px;
background-color: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-family: var(--vp-font-family-mono);
font-size: 12px;
font-weight: 700;
line-height: 18px;
letter-spacing: 0.06em;
}
/* 路径:第二个 code(nth-of-type 只数 code,不受中间的空格文本与
末尾 header-anchor 的 <a> 影响) */
.g3-api .vp-doc h2:has(> code:first-child) > code:nth-of-type(2) {
padding: 0;
background-color: transparent;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
}
/* ==========================================================================
2 & 3. 面板(G3ApiParams / G3ApiPayload)
--------------------------------------------------------------------------
结构:标题栏 + 内容两段式。三个语气走同一组自定义变量,默认中性、
ok / warn / error 分别取 success / warning / danger。
⚠ 语气语义是 **body.code**,不是 HTTP 状态码 —— 这正是通用 OpenAPI
渲染器做不到的地方(它们的颜色模型绑死 HTTP 状态码,会把这里的
失败响应渲染成绿色成功块)。
========================================================================== */
.g3-api-panel {
--g3-api-accent: var(--vp-c-text-3);
--g3-api-accent-soft: var(--vp-c-bg-alt);
margin: 16px 0;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
/* 圆角要裁掉子元素底色(标题栏、代码块),但横向滚动不能裁 ——
滚动交给 __body,见下。 */
overflow: hidden;
}
.g3-api-panel--ok {
--g3-api-accent: var(--vp-c-success-1, var(--vp-c-green-1));
--g3-api-accent-soft: var(--vp-c-success-soft, var(--vp-c-green-soft));
}
.g3-api-panel--warn {
--g3-api-accent: var(--vp-c-warning-1, var(--vp-c-yellow-1));
--g3-api-accent-soft: var(--vp-c-warning-soft, var(--vp-c-yellow-soft));
}
.g3-api-panel--error {
--g3-api-accent: var(--vp-c-danger-1, var(--vp-c-red-1));
--g3-api-accent-soft: var(--vp-c-danger-soft, var(--vp-c-red-soft));
}
.g3-api-panel__head {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background-color: var(--vp-c-bg-soft);
border-block-end: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
font-size: 12px;
font-weight: 600;
line-height: 20px;
}
/* 语气点:与语气同色。文字标签(成功/失败)与它并存,
不让颜色成为唯一的区分手段。 */
.g3-api-panel__dot {
flex: 0 0 auto;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--g3-api-accent);
}
.g3-api-panel--ok .g3-api-panel__head,
.g3-api-panel--warn .g3-api-panel__head,
.g3-api-panel--error .g3-api-panel__head {
color: var(--g3-api-accent);
}
.g3-api-panel__tone {
font-weight: 500;
opacity: 0.9;
}
/* code 徽章:仅 G3ApiPayload 传了 code 时出现 */
.g3-api-panel__code {
padding: 1px 7px;
border-radius: 9999px;
background-color: var(--g3-api-accent-soft);
color: inherit;
font-family: var(--vp-font-family-mono);
font-size: 11px;
font-weight: 600;
}
.g3-api-panel__body {
/* 宽表横向滚动由这一层承担:VitePress 给 .vp-doc table 设了
display:block + overflow-x:auto,那会让 table 自己成为滚动容器
(custom.css 里已记录过它导致 sticky 表头失效的问题)。 */
overflow-x: auto;
}
/* --- 参数表:去掉 .vp-doc 给 markdown 表格画的四边框与上下留白 ------ */
.g3-api-panel__body table {
display: table;
width: 100%;
margin: 0;
}
.g3-api-panel__body table th,
.g3-api-panel__body table td {
border: 0;
border-block-end: 1px solid var(--vp-c-divider);
padding: 8px 14px;
background-color: transparent;
}
.g3-api-panel__body table tr:nth-child(2n) {
background-color: transparent;
}
.g3-api-panel__body table tr:last-child td {
border-block-end: 0;
}
.g3-api-panel__body table th {
color: var(--vp-c-text-3);
font-size: 12px;
font-weight: 600;
text-align: start;
white-space: nowrap;
}
/* 列序约定:参数 / 类型 / 必填 / 说明。这两条按位置定样式,所以要两重限定:
1) variant="params" —— 参数表才有这个形状。bare 的表(错误码:code / 含义 /
典型原因 / 定位)若套上,散文列会被当成「类型」套等宽、被当成「必填」染成品牌色。
2) :has(td:nth-child(4)) —— 同一个 params 变体下,「字段 / 说明」两列表(如
/data/page 的 data 字段)第 2 列是散文,也不该套等宽小字。
第 1 列两种表都是标识符(参数名 / 字段名 / code),所以不加限定。 */
.g3-api-panel__body table td:first-child {
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.g3-api-panel--params .g3-api-panel__body:has(td:nth-child(4)) table td:nth-child(2) {
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
font-size: 12px;
white-space: nowrap;
}
.g3-api-panel--params .g3-api-panel__body:has(td:nth-child(4)) table td:nth-child(3) {
color: var(--vp-c-brand-1);
font-weight: 500;
white-space: nowrap;
}
/* --- 报文里的代码块:贴边(VitePress 默认 margin:20px 0 + 圆角 8px)- */
.g3-api-panel__body div[class*='language-'] {
margin: 0;
border-radius: 0;
}
/* ==========================================================================
4. 表格列宽:短中文列不被压扁
--------------------------------------------------------------------------
现象:/api/getting-started 的「接口一览」里「板块」列被压成一列一个字
(「数据查询」折成两行)。
原因不是这列太长,而是**太短**:CJK 没有「词」的边界,可以逐字换行,于是
该列的 min-content 宽度只有 1 个字。table-layout: auto 按 min-content 分配
空间时,浏览器会把这列压到最小、把余量让给旁边的长路径与长文案 —— 越短的列
越容易被牺牲,与直觉相反。
nowrap 把该列的 min-content 抬到「整格文字的宽度」,即不可再压。
API 页各表的首列都是短键(板块 / 项 / code / 字段 / 参数名),nowrap 安全;
长文案列(说明、触发场景)不带这条规则,照常换行。
========================================================================== */
.g3-api .vp-doc table th:first-child,
.g3-api .vp-doc table td:first-child {
white-space: nowrap;
}
@@ -694,3 +694,80 @@
.vitepress-demo-plugin__container > .vitepress-demo-plugin-preview {
overflow-x: auto;
}
/* --------------------------------------------------------------------------
⚠ 必须还原 <table> 自身:`.vp-doc table { display:block; overflow-x:auto;
margin:20px 0; border-collapse:collapse }`
--------------------------------------------------------------------------
这一整条是 VitePress 给 markdown 表格做「宽表横向滚动 + 上下留白」用的,
但它命中了 G3Table 内部的 `<table class="stk-table-main">`,四个属性逐个都有害:
display:block + overflow-x:auto
→ table 自身成为**滚动容器**
→ th { position: sticky; top: 0 } 的定位基准变成这张 15 万 px 高、
永不滚动的 table,而不是真正在滚的 .stk-table
→ 纵向滚动时**表头跟着一起滚走**
margin:20px 0
→ 表格在容器内整体下移 20px,表头起始位置就偏了 20px
border-collapse:collapse
→ 库要求 separate(它用 border-spacing:0 + 逐格 background-image 画线),
collapse 会改变边框模型
实测对比(同一组件、同一库版本、同样 5000 行虚拟滚动):
playground(无 .vp-doc):滚动 600px 后表头位移 delta=0 ← 正常
文档站(有 .vp-doc) :delta=-600,且 table 计算值 overflow:auto
还原成 table 的默认呈现,让 sticky 的作用域回到 .stk-table 这一层。
⚠ 不要用 `all: revert` 之类的粗暴重置:那会连带清掉库需要的
border-spacing / border-collapse:separate(库显式依赖二者)。 */
.vp-doc .vitepress-demo-plugin-preview table {
display: table;
overflow: visible;
margin: 0;
border-collapse: separate;
}
/* ==========================================================================
组件 Demo:隔断 VitePress 的正文排版样式
--------------------------------------------------------------------------
`.vp-doc` 是 VitePress 给正文(markdown 渲染结果)套的排版容器,它带一条
`.vp-doc th, .vp-doc td { border: 1px solid var(--vp-c-divider) }` —— 给
markdown 表格画的**四边**框线。而 demo 预览区(.vitepress-demo-plugin-preview)
也在 .vp-doc 里面,于是 G3Table 的每个单元格都被额外套上四边框:
实测(/ui/data/table,docs 站):
单元格计算值 borderTop/Bottom/Left/Right 全是 1px
逐像素量内部横线 2px、竖线 2–3px ← 用户看到的「内部边框很厚」
同一组件在 playground(无 .vp-doc)量到的是干净 1px。
也就是说「很厚」= VitePress 的四边框 + 组件自己的行分隔线叠出来的,
组件本身没问题。修在文档站这一层,别去动组件样式。
注意选择器写在 .vp-doc **之内**限定 demo 预览区,不要写全局
`.g3-table th` —— 那会把组件库样式泄漏到宿主页,反过来污染文档站正文里的
普通 markdown 表格。
========================================================================== */
.vp-doc .vitepress-demo-plugin-preview table th,
.vp-doc .vitepress-demo-plugin-preview table td {
border: 0;
padding: 0;
}
/* 内边距交回组件自己:上面 padding:0 是为了清掉 .vp-doc 给 markdown 单元格
设的 padding,但我们的组件用的是 --cell-padding-x,被清掉后文字会贴边,
所以这里还原回库的变量。
⚠ 这里**不要**再写 border-bottom:组件已改为把边框完全交给库
(传 bordered: true),库的 .stk-table.bordered th,td 会用
--bg-border-bottom 渐变自己画底部线。
早先组件用 bordered-v(库不画底部线)时这里补过一条 border-bottom,
现在两者会在同一像素位置叠成 2px —— 正是「内部边框很厚」的来源。 */
.vp-doc .vitepress-demo-plugin-preview .g3-table__inner.stk-table th,
.vp-doc .vitepress-demo-plugin-preview .g3-table__inner.stk-table td {
padding: var(--cell-padding-y) var(--cell-padding-x);
}
/* 展示态/编辑器内容自己带内边距,别被再套一层 */
.vp-doc .vitepress-demo-plugin-preview .g3-table__inner.stk-table td:has(> .g3-table__cell-editor-root) {
padding: 3px;
}
@@ -2,6 +2,10 @@ import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
import './custom.css'
// API 文档版式(content/api/**):端点标题、参数面板、报文面板
import './api.css'
import G3ApiParams from './G3ApiParams.vue'
import G3ApiPayload from './G3ApiPayload.vue'
// 设计令牌的编译产物(经 vite alias 落到 packages/tokens/dist/index.css)。
// 组件样式全部依赖 --g3-* 变量,必须先引入。改 tokens 后需重新 build @g3soft/tokens。
import '@g3soft/tokens'
@@ -9,4 +13,10 @@ import '@g3soft/tokens'
export default {
extends: DefaultTheme,
Layout,
enhanceApp({ app }) {
// API 文档版式组件:全局注册,md 里直接写 <G3ApiParams> / <G3ApiPayload>,
// 与 <demo> 一样是「不 import 就能用」的形态。
app.component('G3ApiParams', G3ApiParams)
app.component('G3ApiPayload', G3ApiPayload)
},
} satisfies Theme