208 lines
4.4 KiB
Vue
208 lines
4.4 KiB
Vue
<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>
|