committed by
GitHub
23 changed files with 558 additions and 38 deletions
@ -0,0 +1,43 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { computed } from 'vue'; |
||||
|
|
||||
|
import PreviewGroup from './preview-group.vue'; |
||||
|
|
||||
|
interface Props { |
||||
|
files?: string; |
||||
|
} |
||||
|
|
||||
|
const props = withDefaults(defineProps<Props>(), { files: '() => []' }); |
||||
|
|
||||
|
const parsedFiles = computed(() => { |
||||
|
try { |
||||
|
return JSON.parse(decodeURIComponent(props.files ?? '')); |
||||
|
} catch { |
||||
|
return []; |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="border-border shadow-float relative rounded-xl border"> |
||||
|
<div |
||||
|
class="not-prose relative w-full overflow-x-auto rounded-t-lg px-4 py-6" |
||||
|
> |
||||
|
<div class="flex w-full max-w-[700px] px-2"> |
||||
|
<slot v-if="parsedFiles.length > 0"></slot> |
||||
|
<div v-else class="text-destructive text-sm"> |
||||
|
<span class="bg-destructive text-foreground rounded-sm px-1 py-1"> |
||||
|
ERROR: |
||||
|
</span> |
||||
|
The preview directory does not exist. Please check the 'dir' |
||||
|
parameter. |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<PreviewGroup v-if="parsedFiles.length > 0" :files="parsedFiles"> |
||||
|
<template v-for="file in parsedFiles" #[file]> |
||||
|
<slot :name="file"></slot> |
||||
|
</template> |
||||
|
</PreviewGroup> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1 @@ |
|||||
|
export { default as DemoPreview } from './demo-preview.vue'; |
||||
@ -0,0 +1,108 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { computed, ref, useSlots } from 'vue'; |
||||
|
|
||||
|
import { VbenTooltip } from '@vben-core/shadcn-ui'; |
||||
|
|
||||
|
import { Code } from 'lucide-vue-next'; |
||||
|
import { |
||||
|
TabsContent, |
||||
|
TabsIndicator, |
||||
|
TabsList, |
||||
|
TabsRoot, |
||||
|
TabsTrigger, |
||||
|
} from 'radix-vue'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
inheritAttrs: false, |
||||
|
}); |
||||
|
|
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
files?: string[]; |
||||
|
}>(), |
||||
|
{ files: () => [] }, |
||||
|
); |
||||
|
|
||||
|
const open = ref(false); |
||||
|
|
||||
|
const slots = useSlots(); |
||||
|
|
||||
|
const tabs = computed(() => { |
||||
|
return props.files.map((file) => { |
||||
|
return { |
||||
|
component: slots[file], |
||||
|
label: file, |
||||
|
}; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
const currentTab = ref('index.vue'); |
||||
|
|
||||
|
const toggleOpen = () => { |
||||
|
open.value = !open.value; |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<TabsRoot |
||||
|
v-model="currentTab" |
||||
|
class="bg-background-deep border-border overflow-hidden rounded-b-xl border-t" |
||||
|
@update:model-value="open = true" |
||||
|
> |
||||
|
<div class="border-border bg-background flex border-b-2 pr-2"> |
||||
|
<div class="flex w-full items-center justify-between text-[13px]"> |
||||
|
<TabsList class="relative flex"> |
||||
|
<template v-if="open"> |
||||
|
<TabsIndicator |
||||
|
class="absolute bottom-0 left-0 h-[2px] w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] rounded-full transition-[width,transform] duration-300" |
||||
|
> |
||||
|
<div class="size-full bg-[var(--vp-c-indigo-1)]"></div> |
||||
|
</TabsIndicator> |
||||
|
<TabsTrigger |
||||
|
v-for="(tab, index) in tabs" |
||||
|
:key="index" |
||||
|
:value="tab.label" |
||||
|
class="border-box text-foreground px-4 py-3 data-[state=active]:text-[var(--vp-c-indigo-1)]" |
||||
|
tabindex="-1" |
||||
|
> |
||||
|
{{ tab.label }} |
||||
|
</TabsTrigger> |
||||
|
</template> |
||||
|
</TabsList> |
||||
|
|
||||
|
<div |
||||
|
:class="{ |
||||
|
'py-2': !open, |
||||
|
}" |
||||
|
class="flex items-center" |
||||
|
> |
||||
|
<VbenTooltip side="top"> |
||||
|
<template #trigger> |
||||
|
<Code |
||||
|
class="hover:bg-accent size-6.5 cursor-pointer rounded-full p-1.5" |
||||
|
@click="toggleOpen" |
||||
|
/> |
||||
|
</template> |
||||
|
{{ open ? 'Collapse code' : 'Expand code' }} |
||||
|
</VbenTooltip> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div |
||||
|
:class="`${open ? 'h-[unset] max-h-[80vh]' : 'h-0'}`" |
||||
|
class="block overflow-y-scroll bg-[var(--vp-code-block-bg)] transition-all duration-300" |
||||
|
> |
||||
|
<TabsContent |
||||
|
v-for="tab in tabs" |
||||
|
:key="tab.label" |
||||
|
:value="tab.label" |
||||
|
as-child |
||||
|
class="rounded-xl" |
||||
|
> |
||||
|
<div class="text-foreground relative rounded-xl"> |
||||
|
<component :is="tab.component" class="border-0" /> |
||||
|
</div> |
||||
|
</TabsContent> |
||||
|
</div> |
||||
|
</TabsRoot> |
||||
|
</template> |
||||
@ -0,0 +1,135 @@ |
|||||
|
import type { MarkdownEnv, MarkdownRenderer } from 'vitepress'; |
||||
|
|
||||
|
import crypto from 'node:crypto'; |
||||
|
import { readdirSync } from 'node:fs'; |
||||
|
import { join } from 'node:path'; |
||||
|
|
||||
|
export const rawPathRegexp = |
||||
|
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/strict
|
||||
|
/^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/; |
||||
|
|
||||
|
function rawPathToToken(rawPath: string) { |
||||
|
const [ |
||||
|
filepath = '', |
||||
|
extension = '', |
||||
|
region = '', |
||||
|
lines = '', |
||||
|
lang = '', |
||||
|
rawTitle = '', |
||||
|
] = (rawPathRegexp.exec(rawPath) || []).slice(1); |
||||
|
|
||||
|
const title = rawTitle || filepath.split('/').pop() || ''; |
||||
|
|
||||
|
return { extension, filepath, lang, lines, region, title }; |
||||
|
} |
||||
|
|
||||
|
export const demoPreviewPlugin = (md: MarkdownRenderer) => { |
||||
|
md.core.ruler.after('inline', 'demo-preview', (state) => { |
||||
|
const insertComponentImport = (importString: string) => { |
||||
|
const index = state.tokens.findIndex( |
||||
|
(i) => i.type === 'html_block' && i.content.match(/<script setup>/g), |
||||
|
); |
||||
|
if (index === -1) { |
||||
|
const importComponent = new state.Token('html_block', '', 0); |
||||
|
importComponent.content = `<script setup>\n${importString}\n</script>\n`; |
||||
|
state.tokens.splice(0, 0, importComponent); |
||||
|
} else { |
||||
|
if (state.tokens[index]) { |
||||
|
const content = state.tokens[index].content; |
||||
|
state.tokens[index].content = content.replace( |
||||
|
'</script>', |
||||
|
`${importString}\n</script>`, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
// Define the regular expression to match the desired pattern
|
||||
|
const regex = /<DemoPreview[^>]*\sdir="([^"]*)"/g; |
||||
|
// Iterate through the Markdown content and replace the pattern
|
||||
|
state.src = state.src.replaceAll(regex, (_match, dir) => { |
||||
|
const componentDir = join(process.cwd(), 'src', dir); |
||||
|
|
||||
|
let childFiles: string[] = []; |
||||
|
let dirExists = true; |
||||
|
|
||||
|
try { |
||||
|
childFiles = |
||||
|
readdirSync(componentDir, { |
||||
|
encoding: 'utf8', |
||||
|
recursive: false, |
||||
|
withFileTypes: false, |
||||
|
}) || []; |
||||
|
} catch { |
||||
|
dirExists = false; |
||||
|
} |
||||
|
|
||||
|
if (!dirExists) { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
const uniqueWord = generateContentHash(componentDir); |
||||
|
|
||||
|
const ComponentName = `DemoComponent_${uniqueWord}`; |
||||
|
insertComponentImport( |
||||
|
`import ${ComponentName} from '${componentDir}/index.vue'`, |
||||
|
); |
||||
|
const { path: _path } = state.env as MarkdownEnv; |
||||
|
|
||||
|
const index = state.tokens.findIndex((i) => i.content.match(regex)); |
||||
|
|
||||
|
if (!state.tokens[index]) { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
state.tokens[index].content = |
||||
|
`<DemoPreview files="${encodeURIComponent(JSON.stringify(childFiles))}" ><${ComponentName}/>
|
||||
|
`;
|
||||
|
|
||||
|
const _dummyToken = new state.Token('', '', 0); |
||||
|
const tokenArray: Array<typeof _dummyToken> = []; |
||||
|
childFiles.forEach((filename) => { |
||||
|
// const slotName = filename.replace(extname(filename), '');
|
||||
|
|
||||
|
const templateStart = new state.Token('html_inline', '', 0); |
||||
|
templateStart.content = `<template #${filename}>`; |
||||
|
tokenArray.push(templateStart); |
||||
|
|
||||
|
const resolvedPath = join(componentDir, filename); |
||||
|
|
||||
|
const { extension, filepath, lang, lines, title } = |
||||
|
rawPathToToken(resolvedPath); |
||||
|
// Add code tokens for each line
|
||||
|
const token = new state.Token('fence', 'code', 0); |
||||
|
token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${ |
||||
|
title ? `[${title}]` : '' |
||||
|
}`;
|
||||
|
|
||||
|
token.content = `<<< ${filepath}`; |
||||
|
(token as any).src = [resolvedPath]; |
||||
|
tokenArray.push(token); |
||||
|
|
||||
|
const templateEnd = new state.Token('html_inline', '', 0); |
||||
|
templateEnd.content = '</template>'; |
||||
|
tokenArray.push(templateEnd); |
||||
|
}); |
||||
|
const endTag = new state.Token('html_inline', '', 0); |
||||
|
endTag.content = '</DemoPreview>'; |
||||
|
tokenArray.push(endTag); |
||||
|
|
||||
|
state.tokens.splice(index + 1, 0, ...tokenArray); |
||||
|
|
||||
|
// console.log(
|
||||
|
// state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
|
||||
|
// );
|
||||
|
return ''; |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
function generateContentHash(input: string, length: number = 10): string { |
||||
|
// 使用 SHA-256 生成哈希值
|
||||
|
const hash = crypto.createHash('sha256').update(input).digest('hex'); |
||||
|
|
||||
|
// 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
|
||||
|
return Number.parseInt(hash, 16).toString(36).slice(0, length); |
||||
|
} |
||||
@ -1,2 +1,3 @@ |
|||||
import './variables.css'; |
import './variables.css'; |
||||
import './base.css'; |
import './base.css'; |
||||
|
import '@vben/styles'; |
||||
|
|||||
@ -0,0 +1,45 @@ |
|||||
|
--- |
||||
|
outline: deep |
||||
|
--- |
||||
|
|
||||
|
# vben-modal |
||||
|
|
||||
|
::: tip |
||||
|
|
||||
|
文档还在完善中,敬请期待。 |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
框架提供的模态框组件,支持`拖拽`、`全屏`、`自定义`等功能。 |
||||
|
|
||||
|
## 基础用法 |
||||
|
|
||||
|
使用 `useVbenModal` 创建最基于的模态框。 |
||||
|
|
||||
|
<DemoPreview dir="demos/vben-modal/basic" /> |
||||
|
|
||||
|
## 组件抽离 |
||||
|
|
||||
|
modal 内的内容一般业务中,会比较复杂,所以我们可以将 modal 内的内容抽离出来。 |
||||
|
|
||||
|
<DemoPreview dir="demos/vben-modal/extra" /> |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### 属性 |
||||
|
|
||||
|
| 属性名 | 描述 | 类型 | 默认值 | |
||||
|
| ------ | ----- | -------- | ------ | |
||||
|
| title | 标题. | `string` | — | |
||||
|
|
||||
|
### 事件 |
||||
|
|
||||
|
| 事件名 | 描述 | 类型 | |
||||
|
| ------ | ---- | ---- | |
||||
|
| TODO | TODO | TODO | |
||||
|
|
||||
|
### 插槽 |
||||
|
|
||||
|
| 插槽名 | 描述 | |
||||
|
| ------- | ---- | |
||||
|
| default | xx. | |
||||
@ -0,0 +1,11 @@ |
|||||
|
# 介绍 |
||||
|
|
||||
|
::: tip README |
||||
|
|
||||
|
该文档介绍的是框架组件的使用方法、属性、事件等。如果你觉得组件封装的不好,或者不符合你的需求,你可以直接使用原生的组件,或者自己封装一个组件,不需要拘泥于框架提供的组件。我们只是提供了一些常用的组件,方便你快速开发。是否使用,取决于你的需求。 |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
## 通用组件 |
||||
|
|
||||
|
通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。 |
||||
@ -0,0 +1,11 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useVbenModal, VbenButton } from '@vben/common-ui'; |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal(); |
||||
|
</script> |
||||
|
<template> |
||||
|
<div> |
||||
|
<VbenButton @click="() => modalApi.open()">打开弹窗</VbenButton> |
||||
|
<Modal title="基础示例"> modal content </Modal> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,22 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useVbenModal, VbenButton } from '@vben/common-ui'; |
||||
|
|
||||
|
import ExtraModal from './modal.vue'; |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
// 链接抽离的组件 |
||||
|
connectedComponent: ExtraModal, |
||||
|
}); |
||||
|
|
||||
|
function openModal() { |
||||
|
modalApi.open(); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div> |
||||
|
<Modal /> |
||||
|
|
||||
|
<VbenButton @click="openModal">打开弹窗</VbenButton> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,8 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
|
||||
|
const [Modal] = useVbenModal(); |
||||
|
</script> |
||||
|
<template> |
||||
|
<Modal title="基础示例"> extra modal content </Modal> |
||||
|
</template> |
||||
@ -0,0 +1,11 @@ |
|||||
|
import tailwindcssConfig from '@vben/tailwind-config'; |
||||
|
|
||||
|
export default { |
||||
|
...tailwindcssConfig, |
||||
|
content: [ |
||||
|
...tailwindcssConfig.content, |
||||
|
'.vitepress/**/*.{js,mts,ts,vue}', |
||||
|
'src/demos/**/*.{js,mts,ts,vue}', |
||||
|
'src/**/*.md', |
||||
|
], |
||||
|
}; |
||||
@ -1,6 +1,13 @@ |
|||||
{ |
{ |
||||
"$schema": "https://json.schemastore.org/tsconfig", |
"$schema": "https://json.schemastore.org/tsconfig", |
||||
"extends": "@vben/tsconfig/web.json", |
"extends": "@vben/tsconfig/web.json", |
||||
"include": [".vitepress/*.mts", ".vitepress/**/*.ts", ".vitepress/**/*.vue"], |
"include": [ |
||||
|
".vitepress/*.mts", |
||||
|
".vitepress/**/*.ts", |
||||
|
".vitepress/**/*.vue", |
||||
|
"src/*.mts", |
||||
|
"src/**/*.ts", |
||||
|
"src/**/*.vue" |
||||
|
], |
||||
"exclude": ["node_modules"] |
"exclude": ["node_modules"] |
||||
} |
} |
||||
|
|||||
@ -1,3 +1,5 @@ |
|||||
export * from './ellipsis-text'; |
export * from './ellipsis-text'; |
||||
export * from './page'; |
export * from './page'; |
||||
export * from '@vben-core/popup-ui'; |
export * from '@vben-core/popup-ui'; |
||||
|
|
||||
|
export { VbenButton } from '@vben-core/shadcn-ui'; |
||||
|
|||||
Loading…
Reference in new issue