add vitepress docs
This commit is contained in:
@@ -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>
|
||||
Vendored
+16
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 文档站全局样式。
|
||||
*
|
||||
* 只处理 VitePress 正文与演示容器、API 表格的交界处;
|
||||
* 组件自身的样式一律各自 scoped,避免相互污染。
|
||||
*/
|
||||
|
||||
.vp-doc {
|
||||
// 演示容器与正文的呼吸感
|
||||
.tui-demo {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
// API 表格里的类型与默认值不换行,避免长签名把列挤变形
|
||||
table code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user