add vitepress docs

This commit is contained in:
Timi
2026-09-21 16:05:14 +08:00
parent b435bf992e
commit 37cf38fe27
73 changed files with 7003 additions and 802 deletions
@@ -0,0 +1,162 @@
<template>
<div v-if="api" class="tui-api">
<section v-if="api.props.length > 0">
<h3>Props</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr v-for="row in api.props" :key="row.name">
<td class="name">
<code v-text="row.name" />
<span v-if="row.required" class="required" title="必填">*</span>
</td>
<td><code v-text="row.type" /></td>
<td><code v-text="row.defaultValue" /></td>
<td v-text="row.description" />
</tr>
</tbody>
</table>
</section>
<section v-if="api.events.length > 0">
<h3>Events</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>参数</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr v-for="row in api.events" :key="row.name">
<td><code v-text="row.name" /></td>
<td><code v-text="row.type" /></td>
<td v-text="row.description" />
</tr>
</tbody>
</table>
</section>
<section v-if="api.slots.length > 0">
<h3>Slots</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr v-for="row in api.slots" :key="row.name">
<td><code v-text="row.name" /></td>
<td v-text="row.description" />
</tr>
</tbody>
</table>
</section>
<section v-if="api.exposes.length > 0">
<h3>Expose</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr v-for="row in api.exposes" :key="row.name">
<td><code v-text="row.name" /></td>
<td><code v-text="row.type" /></td>
<td v-text="row.description" />
</tr>
</tbody>
</table>
</section>
<p v-if="isEmpty && !api.parsed" class="notice warn">
未能从源码中解析出 API 信息,请检查组件是否使用了 defineProps / defineEmits / defineExpose。
</p>
<p v-else-if="isEmpty" class="notice">
该组件不需要任何 props、events、slots 或实例方法。
</p>
</div>
<p v-else class="notice warn" v-text="`未找到组件 ${name} 的 API 数据,请检查 components.ts 中的 name 与 entry 是否正确`" />
</template>
<script lang="ts" setup>
import { computed } from "vue";
import apiMap from "virtual:timi-api";
/**
* 组件 API 表格。
*
* 数据来自构建期 `vue-docgen-api` 对组件源码的解析结果,
* 无需在 markdown 中手写 props / events / slots 表格。
* 某个分区没有内容时整段不渲染,避免出现空表。
*/
const props = defineProps<{
/** 组件名,需与 components.ts 中的 name 一致。 */
name: string;
}>();
const api = computed(() => apiMap[props.name]);
const isEmpty = computed(() => {
if (!api.value) {
return false;
}
const { props: propRows, events, slots, exposes } = api.value;
return propRows.length === 0 && events.length === 0 && slots.length === 0 && exposes.length === 0;
});
</script>
<style lang="less" scoped>
.tui-api {
section {
margin-bottom: 1.5rem;
&:last-child {
margin-bottom: 0;
}
}
h3 {
margin: 1.5rem 0 .75rem;
font-size: 1rem;
}
.name {
white-space: nowrap;
}
.required {
margin-left: .125rem;
color: var(--vp-c-danger-1);
}
.notice {
margin: 1rem 0;
padding: .75rem 1rem;
font-size: .875rem;
border-radius: .5rem;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
&.warn {
color: var(--vp-c-warning-1);
background: var(--vp-c-warning-soft);
}
}
}
</style>
@@ -0,0 +1,207 @@
<template>
<div class="tui-demo">
<section class="stage">
<div class="viewport">
<slot />
</div>
</section>
<section class="meta">
<div class="info">
<p v-if="title" class="title" v-text="title" />
<p v-if="description" class="desc" v-text="description" />
</div>
<div class="actions">
<button
class="action"
type="button"
@click="handleCopy"
v-text="copied ? '已复制' : '复制代码'"
/>
<button
class="action"
type="button"
@click="toggleExpanded"
v-text="expanded ? '收起代码' : '展开代码'"
/>
</div>
</section>
<section v-show="expanded" class="source">
<div class="language-vue" v-html="sourceHtml" />
</section>
</div>
</template>
<script lang="ts" setup>
import { computed, onUnmounted, ref } from "vue";
/**
* 预览容器。
*
* props 契约由 `@vitepress-demo-preview/plugin` 决定,它把 markdown 里的
* `:::preview` 容器编译成 `<demo-preview>` 标签并传入下列属性:
* - `code`:`encodeURI` 处理过的组件源码
* - `showCode`:`encodeURIComponent` 处理过的高亮 HTML
*
* 这里刻意不依赖官方容器,也不引入额外 UI 库,只用原生元素配合 VitePress 的
* CSS 变量,深色模式随之自动适配。
*
* 演示统一放在手机宽度的视口里:这些是移动端组件,按桌面宽度铺开会失去真实形态。
* 需要调整宽度时改 `--tui-demo-viewport` 即可。
*/
const props = withDefaults(defineProps<{
/** 源码原文,经 encodeURI 编码。 */
code?: string;
/** 高亮后的源码 HTML,经 encodeURIComponent 编码。 */
showCode?: string;
/** 演示标题,来自 `:::preview` 后方第一段文字。 */
title?: string;
/** 演示描述,来自 `:::preview` 中 `||` 之后的内容。 */
description?: string;
}>(), {
code: "",
showCode: "",
title: "",
description: ""
});
/** 源码区是否展开,默认收起以保持文档阅读节奏。 */
const expanded = ref(false);
/** 复制成功后的短暂反馈。 */
const copied = ref(false);
let copiedTimer: ReturnType<typeof setTimeout> | null = null;
const sourceHtml = computed(() => decodeURIComponent(props.showCode));
const sourceText = computed(() => decodeURIComponent(props.code));
/** 复制源码到剪贴板,clipboard 不可用时保持原按钮文案。 */
async function handleCopy() {
try {
await navigator.clipboard.writeText(sourceText.value);
} catch {
return;
}
copied.value = true;
if (copiedTimer !== null) {
clearTimeout(copiedTimer);
}
copiedTimer = setTimeout(() => {
copied.value = false;
copiedTimer = null;
}, 1500);
}
function toggleExpanded() {
expanded.value = !expanded.value;
}
onUnmounted(() => {
if (copiedTimer !== null) {
clearTimeout(copiedTimer);
}
});
</script>
<style lang="less" scoped>
.tui-demo {
overflow: hidden;
border: 1px solid var(--vp-c-divider);
border-radius: .75rem;
background: var(--vp-c-bg);
.stage {
display: flex;
padding: 1.5rem;
justify-content: center;
background: var(--vp-c-bg-soft);
}
// 手机视口:移动组件在桌面宽度下会被拉伸,这里统一收到移动端宽度
.viewport {
width: 100%;
max-width: var(--tui-demo-viewport, 26rem);
padding: 1rem;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
border-radius: .75rem;
background: var(--vp-c-bg);
}
.meta {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: .75rem 1.5rem;
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.info {
min-width: 0;
}
.title {
margin: 0;
font-size: .875rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.desc {
margin: .25rem 0 0;
font-size: .75rem;
line-height: 1.5;
color: var(--vp-c-text-2);
}
.actions {
display: flex;
flex: none;
gap: .25rem;
}
.action {
padding: .25rem .5rem;
font-size: .75rem;
cursor: pointer;
border: none;
border-radius: .25rem;
color: var(--vp-c-text-2);
background: transparent;
&:hover {
color: var(--vp-c-brand-1);
background: var(--vp-c-default-soft);
}
}
.source {
border-top: 1px solid var(--vp-c-divider);
:deep(div[class*="language-"]) {
margin: 0;
border-radius: 0;
}
}
}
@media (max-width: 48rem) {
.tui-demo {
.stage {
padding: .75rem;
}
.viewport {
padding: .5rem;
}
.meta {
align-items: flex-start;
flex-direction: column;
padding: .75rem 1rem;
}
}
}
</style>
+16
View File
@@ -0,0 +1,16 @@
/// <reference types="vitepress/client" />
/** 构建期由 vue-docgen-api 解析生成的组件 API 数据。 */
declare module "virtual:timi-api" {
import type { ComponentApiMap } from "../api-types";
const apiMap: ComponentApiMap;
export default apiMap;
}
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
export default component;
}
+48
View File
@@ -0,0 +1,48 @@
import DefaultTheme from "vitepress/theme";
import { watch } from "vue";
import { useData } from "vitepress";
import type { App } from "vue";
import TDesignMobile from "tdesign-mobile-vue";
import TimiWebUI, { VPopup } from "timi-web";
// 必须引完整样式:`tdesign-mobile-vue/es/style/index.css` 只含全局 token,
// 不含任何组件类,全量注册时会导致所有组件无样式渲染。
import "tdesign-mobile-vue/dist/tdesign.css";
import "./styles/index.less";
import DemoPreview from "./containers/DemoPreview.vue";
import ApiTable from "./components/ApiTable.vue";
/**
* 同步深色模式。
*
* VitePress 通过 `html.dark` 切换深色,而 TDesign 依赖 `html[theme-mode="dark"]`,
* 两者不是同一套机制,不同步会导致深色模式下 TDesign 组件仍是亮色。
*/
function syncThemeMode(dark: boolean) {
if (typeof document === "undefined") {
return;
}
document.documentElement.setAttribute("theme-mode", dark ? "dark" : "light");
}
export default {
extends: DefaultTheme,
enhanceApp({ app }: { app: App }) {
// 全量注册 TDesign Mobile,演示区内所有 t-* 组件直接可用。
app.use(TDesignMobile);
// 组件内部大量使用 timi-web 的 v-popup 指令,且用在组件模板里,
// 无法由演示局部注入,只能在主题里全局注册。
app.use(TimiWebUI);
app.directive("popup", VPopup);
app.component("demo-preview", DemoPreview);
app.component("api-table", ApiTable);
},
setup() {
const { isDark } = useData();
watch(isDark, syncThemeMode, { immediate: true });
}
};
+18
View File
@@ -0,0 +1,18 @@
/**
* 文档站全局样式。
*
* 只处理 VitePress 正文与演示容器、API 表格的交界处;
* 组件自身的样式一律各自 scoped,避免相互污染。
*/
.vp-doc {
// 演示容器与正文的呼吸感
.tui-demo {
margin: 1rem 0;
}
// API 表格里的类型与默认值不换行,避免长签名把列挤变形
table code {
white-space: nowrap;
}
}