<template>
<div ref="demoContainer" :id="demo.demoId" class="demo-container">
<div
v-if="isIntersecting"
class="ti-br-sm ti-wp100"
:class="currDemoId === demo.demoId ? 'b-a-success is-current' : ''"
>
<div class="demo-content">
<div class="ti-f-r ti-f-pos-between ti-f-box-end">
<h2 class="demo-title">{{ demo.name[state.langKey] }}</h2>
<div class="demo-options">
<i
v-auto-tip="{ content: state.copyTip, effect: 'light', always: true }"
:class="state.copyIcon"
class="ti-w16 ti-h16 ti-cur-hand"
@click="copyCode(demo)"
@mouseout="resetTip()"
/>
<i
v-auto-tip="{
content: demo.isOpen ? i18nByKey('hideCode') : i18nByKey('showCode'),
effect: 'light',
always: true
}"
:class="!!demo.isOpen ? 'i-ti-codeslash' : 'i-ti-code'"
class="ti-ml8 ti-w16 ti-h16 ti-cur-hand"
@click="toggleDemoCode(demo)"
/>
<i
v-if="!isPlus"
v-auto-tip="{ content: i18nByKey('playground'), effect: 'light', always: true }"
class="i-ti-playground ml8 ti-w16 ti-h16 ti-cur-hand"
@click="openPlayground(demo)"
/>
</div>
</div>
<component :is="getDescMd(demo)" class="demo-desc" />
<div v-if="demoConfig.isMobile" class="pc-demo-container">
<div class="mobile-view-btn">
<tiny-button @click="openIframe(demo)">{{ i18nByKey('yan-shi') }}</tiny-button>
</div>
</div>
<div v-else class="pc-demo-container">
<div class="pc-demo">
<component :is="cmp" />
</div>
</div>
</div>
<div v-if="demo.isOpen" class="ti-px24 ti-py20 ti-b-t-lightless demo-code">
<template v-if="files?.length">
<tiny-tabs v-model="state.tabValue" class="code-tabs">
<tiny-tab-item v-for="(file, idx) in files" :key="file.fileName" :name="'tab' + idx" :title="file.fileName">
<async-highlight :code="file.code" :filename="file.fileName"></async-highlight>
</tiny-tab-item>
</tiny-tabs>
</template>
<div v-else-if="files[0]">
<async-highlight :code="files[0].code" :filename="files[0].fileName"></async-highlight>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ref,
reactive,
computed,
shallowRef,
onMounted,
onBeforeUnmount,
watch,
nextTick,
inject,
h,
provide
} from 'vue'
import { i18nByKey, getWord } from '@/i18n'
import { $split, fetchDemosFile } from '@/tools'
import { Tabs as TinyTabs, TabItem as TinyTabItem, Button as TinyButton } from '@opentiny/vue'
import { AutoTip as vAutoTip } from '@opentiny/vue-directive'
import { languageMap, vueComponents, getWebdocPath, staticDemoPath } from '../views/components-doc/cmp-config'
import { router } from '@/router.js'
import demoConfig from '@demos/config.js'
import { useApiMode, useTemplateMode } from '@/tools'
import useTheme from '@/tools/useTheme'
import AsyncHighlight from './async-highlight.vue'
const props = defineProps({
demo: {
type: Object,
default: () => ({})
},
currDemoId: {
type: String,
default: ''
},
observer: {
type: Object,
default: () => ({})
},
isIntersecting: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['mounted', 'get-iframe-demo'])
const { apiModeState, apiModeFn } = useApiMode()
defineOptions({
name: 'Demo'
})
const demos = ref(props.demo)
const files = ref([])
const { templateModeState } = useTemplateMode()
const { currentThemeKey } = useTheme()
const isMobileFirst = computed(() => {
return templateModeState.mode === 'mobile-first'
})
const isPlus = computed(() => import.meta.env.VITE_APP_MODE === 'plus')
const demoContainer = ref(null)
const cmp = shallowRef(null)
const showPreview = inject('showPreview')
const state = reactive({
tabValue: 'tab0',
cmpId: router.currentRoute.value.params.cmpId,
langKey: getWord('zh-CN', 'en-US', 'es-LA', 'pt-BR'),
copyTip: i18nByKey('copyCode'),
copyIcon: 'i-ti-copy'
})
watch(
() => props.isIntersecting,
async () => {
if (props.isIntersecting) {
const demoName = apiModeFn.getDemoName(`${getWebdocPath(state.cmpId)}/${props.demo.codeFiles[0]}`)
if (vueComponents[demoName]) {
cmp.value = (await vueComponents[demoName]()).default
} else {
const log = `${demoName}示例资源不存在,请检查文件名是否正确?`
cmp.value = h('div', {}, log)
}
}
},
{ immediate: true }
)
watch(
() => props.demo,
() => {
demos.value = props.demo
if (props.demo.files) {
files.value = props.demo.files
}
},
{ deep: true }
)
watch(
() => apiModeState.apiMode,
() => {
if (props.demo.files?.length > 0) {
getDemoCodeFn(props.demo, true).then((demoFiles) => {
files.value = demoFiles
})
}
}
)
const getDemoCodeFn = async (demo, forceUpdate?: boolean) => {
if (!demo.files || forceUpdate) {
const cmpId = router.currentRoute.value.params.cmpId
const promises = demo.codeFiles.map(async (fileName) => {
const demoName = apiModeFn.getDemoName(`${getWebdocPath(cmpId)}/${fileName}`)
let code = ''
const path = isMobileFirst.value
? `@demos/mobile-first/app/${demoName.replace('mobile-first/', '')}`
: `${staticDemoPath}/${demoName}`
code = await fetchDemosFile(path)
.then((code) => {
return code
})
.catch(() => {
return `${demoName}示例资源不存在,请检查文件名是否正确?`
})
const ext = $split(fileName, '.', -1)
const language = languageMap[ext] || ''
return { code, fileName, language }
})
demo.files = await Promise.all(promises)
return demo.files
}
return demo.files
}
const getDescMd = (demo) => {
const desc = demo.desc[state.langKey].trim()
const html = h('div', { class: 'demo-desc', innerHTML: desc })
return html
}
const toggleDemoCode = async (demo) => {
if (!demo.files) {
await getDemoCode(demo)
demo.isOpen = true
} else {
demo.isOpen = !demo.isOpen
}
await nextTick()
window.dispatchEvent(new Event('resize'))
}
const copyCode = async (demo) => {
if (demo.isOpen) {
const idx = parseInt(state.tabValue.slice(3))
navigator.clipboard.writeText(demo.files[idx].code)
} else {
await getDemoCode(demo)
navigator.clipboard.writeText(demo.files[0].code)
}
state.copyTip = i18nByKey('copyCodeOk')
state.copyIcon = 'i-ti-check'
}
const resetTip = () => {
setTimeout(() => {
state.copyTip = i18nByKey('copyCode')
state.copyIcon = 'i-ti-copy'
}, 300)
}
const getDemoCode = (demo) => {
return getDemoCodeFn(demo)
}
const openPlayground = (demo, open = true) => {
const cmpId = router.currentRoute.value.params.cmpId
const tinyTheme = templateModeState.isSaas ? 'saas' : currentThemeKey.value.split('-')[0]
const openModeQuery = open ? '' : '&openMode=preview'
const url = `${import.meta.env.VITE_PLAYGROUND_URL}?cmpId=${cmpId}&fileName=${demo.codeFiles[0]}&apiMode=${
isMobileFirst.value ? 'Options' : apiModeState.apiMode
}&mode=${templateModeState.mode}&theme=${tinyTheme}${openModeQuery}`
if (open) {
window.open(url)
} else {
showPreview(url)
}
}
const openIframe = (demo) => {
emit('get-iframe-demo', demo)
}
provide('TinyMode', templateModeState.mode)
onMounted(() => {
if (demoContainer.value) {
props.observer?.observe?.(demoContainer.value)
}
nextTick(() => {
emit('mounted')
})
})
onBeforeUnmount(() => {
if (demoContainer.value) {
props.observer?.unobserve?.(demoContainer.value)
}
})
</script>
<style lang="less" scoped>
.demo-container {
min-height: 200px;
width: 100%;
}
.is-current {
padding: 20px 24px;
}
.demo-desc {
font-size: 16px;
line-height: 1.7em;
margin: 8px 0 24px;
:deep(code) {
color: #476582;
padding: 4px 8px;
margin: 0 4px;
font-size: 0.85em;
background-color: var(--tv-color-bg-header, #f5f7fa);
border-radius: 3px;
}
a {
text-decoration: none;
color: #5e7ce0;
cursor: pointer;
}
ul,
ol {
list-style: disc;
list-style-position: inside;
}
}
.b-a-success {
animation: border-trans 3s;
}
@keyframes border-trans {
0% {
border: 1px solid #5073e5;
background: none;
}
50% {
background: rgba(255, 95, 85, 0.1);
border: 1px solid rgba(255, 95, 88, 0.6);
}
100% {
border: 1px solid #5073e5;
background: none;
}
}
@media screen and (max-width: 640px) {
.pc-demo-container {
overflow: auto;
}
}
.pc-demo-container {
display: flex;
flex-direction: column;
color: var(--tv-color-text);
background: #fff;
border-radius: 6px;
border: 1px solid #dcdfe6;
padding: 26px 18px 42px;
.pc-demo {
flex: 1;
padding: 6px;
}
}
.demo-content {
position: relative;
.demo-title {
font-size: 20px;
color: var(--tv-color-text);
font-weight: bold;
}
.demo-desc {
margin-bottom: 16px;
font-size: 14px;
color: #595959;
line-height: 30px;
}
.demo-options {
height: 16px;
position: absolute;
right: 32px;
bottom: 16px;
i {
color: var(--tv-color-text);
&:hover {
color: var(--tv-color-act-success-text-hover);
}
}
}
}
.demo-code {
border: 1px solid #efeff4;
}
.code-tabs {
.tiny-tabs__content {
padding: 0;
}
}
.code-preview-box {
overflow: auto;
padding: 20px 5px;
}
</style>
<style lang="less">
html.dark .pc-demo-container.pc-demo-container {
background-color: #1a1a1a;
border: none;
}
html.dark .demo-content.demo-content .demo-desc {
color: #b3b3b3;
}
</style>