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,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>