feat(frontend): 引入可视化Markdown写作与Shiki高亮
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
|
||||
const props = defineProps<{ source: string }>()
|
||||
const html = ref('')
|
||||
let renderVersion = 0
|
||||
|
||||
watch(() => props.source, async (source) => {
|
||||
const version = ++renderVersion
|
||||
const result = await renderMarkdown(source)
|
||||
if (version === renderVersion) html.value = result
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markdown-content" v-html="html" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.markdown-content { white-space: normal; user-select: text; }
|
||||
.markdown-content :deep(p), .markdown-content :deep(ul), .markdown-content :deep(ol), .markdown-content :deep(pre), .markdown-content :deep(blockquote) { margin: .65em 0; }
|
||||
.markdown-content :deep(h1), .markdown-content :deep(h2), .markdown-content :deep(h3) { margin: 1em 0 .5em; line-height: var(--line-height-tight); }
|
||||
.markdown-content :deep(ul) { padding-left: 1.5em; list-style: disc; }
|
||||
.markdown-content :deep(ol) { padding-left: 1.5em; list-style: decimal; }
|
||||
.markdown-content :deep(.shiki) { overflow: auto; padding: var(--space-md); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); }
|
||||
.markdown-content :deep(code) { padding: .1em .3em; border-radius: var(--radius-sm); background: var(--color-background-tertiary); font-family: var(--font-ui-mono); }
|
||||
.markdown-content :deep(pre code) { padding: 0; background: transparent; }
|
||||
.markdown-content :deep(blockquote) { padding-left: 1em; border-left: 3px solid var(--color-accent-primary); color: var(--color-text-secondary); }
|
||||
.markdown-content :deep(table) { width: 100%; margin: .65em 0; border-collapse: collapse; }
|
||||
.markdown-content :deep(th), .markdown-content :deep(td) { padding: .45em .65em; border: 1px solid var(--color-border-default); text-align: left; }
|
||||
.markdown-content :deep(img) { max-width: 100%; }
|
||||
.markdown-content :deep(hr) { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border-default); }
|
||||
:global([data-theme='dark']) .markdown-content :deep(.shiki),
|
||||
:global([data-theme='dark']) .markdown-content :deep(.shiki span) {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@ import { useEditorStore } from '@/stores/editor'
|
||||
import { useProviderStore } from '@/stores/provider'
|
||||
import { useSkillStore } from '@/stores/skill'
|
||||
import { useWorkspaceStore } from '@/stores/workspace'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
import MarkdownContent from '@/components/common/MarkdownContent.vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const providerStore = useProviderStore()
|
||||
@@ -69,7 +69,7 @@ async function openCitation(citation: Citation) {
|
||||
<div class="avatar">{{ message.role === 'user' ? '你' : 'AI' }}</div>
|
||||
<div class="message-body">
|
||||
<details v-if="message.thinking" class="thinking"><summary>思考过程</summary><p>{{ message.thinking }}</p></details>
|
||||
<div v-if="message.content" class="message-content markdown-content" v-html="renderMarkdown(message.content)" />
|
||||
<MarkdownContent v-if="message.content" class="message-content" :source="message.content" />
|
||||
<div v-else-if="chatStore.isStreaming" class="message-content">正在思考…</div>
|
||||
<div v-if="message.tool_calls?.length" class="tool-calls"><div v-for="call in message.tool_calls" :key="call.tool_call_id" class="item-card"><span class="badge info">{{ call.status }}</span><strong>{{ call.name }}</strong><pre>{{ JSON.stringify(call.parameters, null, 2) }}</pre></div></div>
|
||||
<div v-if="message.citations?.length" class="citations">
|
||||
@@ -104,15 +104,6 @@ async function openCitation(citation: Citation) {
|
||||
.avatar { display: grid; place-items: center; width: 34px; height: 34px; border-radius: var(--radius-full); background: var(--color-background-tertiary); font-weight: 700; }
|
||||
.assistant .avatar { background: var(--color-accent-soft); color: var(--color-accent-primary); }
|
||||
.message-content { white-space: pre-wrap; line-height: var(--line-height-relaxed); }
|
||||
.markdown-content { white-space: normal; user-select: text; }
|
||||
.markdown-content :deep(p), .markdown-content :deep(ul), .markdown-content :deep(ol), .markdown-content :deep(pre), .markdown-content :deep(blockquote) { margin: .65em 0; }
|
||||
.markdown-content :deep(h1), .markdown-content :deep(h2), .markdown-content :deep(h3) { margin: 1em 0 .5em; line-height: var(--line-height-tight); }
|
||||
.markdown-content :deep(ul) { padding-left: 1.5em; list-style: disc; }.markdown-content :deep(ol) { padding-left: 1.5em; list-style: decimal; }
|
||||
.markdown-content :deep(pre) { overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-background-secondary); }
|
||||
.markdown-content :deep(code) { padding: .1em .3em; border-radius: var(--radius-sm); background: var(--color-background-tertiary); font-family: var(--font-ui-mono); }.markdown-content :deep(pre code) { padding: 0; background: transparent; }
|
||||
.markdown-content :deep(blockquote) { padding-left: 1em; border-left: 3px solid var(--color-accent-primary); color: var(--color-text-secondary); }
|
||||
.markdown-content :deep(table) { width: 100%; margin: .65em 0; border-collapse: collapse; }.markdown-content :deep(th), .markdown-content :deep(td) { padding: .45em .65em; border: 1px solid var(--color-border-default); text-align: left; }
|
||||
.markdown-content :deep(img) { max-width: 100%; }.markdown-content :deep(hr) { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border-default); }
|
||||
.thinking { margin-bottom: var(--space-sm); color: var(--color-text-secondary); }.thinking p { margin-top: var(--space-sm); white-space: pre-wrap; }
|
||||
.tool-calls { display: grid; gap: var(--space-sm); margin-top: var(--space-md); }.tool-calls .item-card { display: grid; gap: var(--space-xs); }.tool-calls pre { overflow: auto; font-size: var(--font-size-xs); }
|
||||
.usage { display: block; margin-top: var(--space-xs); color: var(--color-text-tertiary); }
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
import VisualMarkdownEditor from './VisualMarkdownEditor.vue'
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const renderedContent = computed(() => renderMarkdown(editorStore.content))
|
||||
|
||||
function updateContent(event: Event) {
|
||||
editorStore.updateContent((event.target as HTMLTextAreaElement).value)
|
||||
editorStore.scheduleAutoSave(settingsStore.autoSaveInterval)
|
||||
@@ -15,11 +12,8 @@ function updateContent(event: Event) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="editorStore.mode === 'wysiwyg'" class="writing-layout">
|
||||
<textarea class="editor-pane writing-input" :value="editorStore.content" :spellcheck="settingsStore.spellCheck"
|
||||
aria-label="Markdown 写作编辑器" @input="updateContent" />
|
||||
<article class="markdown-preview" aria-label="Markdown 实时预览" v-html="renderedContent" />
|
||||
</div>
|
||||
<VisualMarkdownEditor v-if="editorStore.mode === 'wysiwyg'" :key="editorStore.currentFilePath ?? 'empty'"
|
||||
:initial-content="editorStore.content" />
|
||||
<textarea v-else class="editor-pane source" :value="editorStore.content" :spellcheck="false"
|
||||
aria-label="Markdown 源码编辑器" @input="updateContent" />
|
||||
</template>
|
||||
@@ -39,21 +33,5 @@ function updateContent(event: Event) {
|
||||
line-height: 1.7;
|
||||
user-select: text;
|
||||
}
|
||||
.writing-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); flex: 1; min-height: 0; }
|
||||
.writing-input { border-right: 1px solid var(--color-border-default); font-family: var(--font-editor-sans); font-size: var(--font-editor-size); line-height: var(--font-editor-line-height); }
|
||||
.markdown-preview { width: min(100%, var(--editor-line-width, 80ch)); overflow: auto; margin: 0 auto; padding: var(--space-3xl) var(--space-xl); font-family: var(--font-editor-sans); font-size: var(--font-editor-size); line-height: var(--font-editor-line-height); user-select: text; }
|
||||
.markdown-preview :deep(h1), .markdown-preview :deep(h2), .markdown-preview :deep(h3) { margin: 1.4em 0 .6em; line-height: var(--line-height-tight); color: var(--color-text-primary); }
|
||||
.markdown-preview :deep(h1:first-child), .markdown-preview :deep(h2:first-child) { margin-top: 0; }
|
||||
.markdown-preview :deep(p), .markdown-preview :deep(ul), .markdown-preview :deep(ol), .markdown-preview :deep(blockquote), .markdown-preview :deep(pre), .markdown-preview :deep(table) { margin: .8em 0; }
|
||||
.markdown-preview :deep(ul), .markdown-preview :deep(ol) { padding-left: 1.6em; }
|
||||
.markdown-preview :deep(ul) { list-style: disc; }.markdown-preview :deep(ol) { list-style: decimal; }
|
||||
.markdown-preview :deep(blockquote) { padding-left: 1em; border-left: 3px solid var(--color-accent-primary); color: var(--color-text-secondary); }
|
||||
.markdown-preview :deep(code) { padding: .15em .35em; border-radius: var(--radius-sm); background: var(--color-background-tertiary); font-family: var(--font-editor-mono); }
|
||||
.markdown-preview :deep(pre) { overflow: auto; padding: var(--space-lg); border-radius: var(--radius-md); background: var(--color-background-secondary); }
|
||||
.markdown-preview :deep(pre code) { padding: 0; background: transparent; }
|
||||
.markdown-preview :deep(table) { width: 100%; border-collapse: collapse; }.markdown-preview :deep(th), .markdown-preview :deep(td) { padding: .5em .7em; border: 1px solid var(--color-border-default); text-align: left; }
|
||||
.markdown-preview :deep(img) { max-width: 100%; }.markdown-preview :deep(a) { color: var(--color-text-link); }
|
||||
.markdown-preview :deep(hr) { margin: 1.5em 0; border: 0; border-top: 1px solid var(--color-border-default); }
|
||||
.editor-pane.source { font-family: var(--font-editor-mono); font-size: var(--font-editor-size); line-height: var(--font-editor-line-height); }
|
||||
@media (max-width: 900px) { .writing-layout { grid-template-columns: 1fr; grid-template-rows: minmax(220px, 1fr) minmax(220px, 1fr); overflow: auto; }.writing-input { min-height: 220px; border-right: 0; border-bottom: 1px solid var(--color-border-default); }.markdown-preview { min-height: 220px; } }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { CollectionTag, Edit, EditPen, List, Memo } from '@element-plus/icons-vue'
|
||||
import { Crepe } from '@milkdown/crepe'
|
||||
import {
|
||||
toggleEmphasisCommand,
|
||||
toggleStrongCommand,
|
||||
wrapInBulletListCommand,
|
||||
wrapInHeadingCommand,
|
||||
wrapInOrderedListCommand,
|
||||
} from '@milkdown/kit/preset/commonmark'
|
||||
import { callCommand } from '@milkdown/kit/utils'
|
||||
import AppIcon from '@/components/common/AppIcon.vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { highlightCode } from '@/utils/markdown'
|
||||
import '@milkdown/crepe/theme/common/style.css'
|
||||
import '@milkdown/crepe/theme/frame.css'
|
||||
|
||||
const props = defineProps<{ initialContent: string }>()
|
||||
const editorStore = useEditorStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const editorRoot = ref<HTMLElement | null>(null)
|
||||
const loading = ref(true)
|
||||
let crepe: Crepe | null = null
|
||||
|
||||
type ToolbarCommand = 'heading' | 'bold' | 'italic' | 'ordered-list' | 'bullet-list'
|
||||
|
||||
function runCommand(command: ToolbarCommand) {
|
||||
const editor = crepe?.editor
|
||||
if (!editor) return
|
||||
const actions = {
|
||||
heading: callCommand(wrapInHeadingCommand.key, 2),
|
||||
bold: callCommand(toggleStrongCommand.key),
|
||||
italic: callCommand(toggleEmphasisCommand.key),
|
||||
'ordered-list': callCommand(wrapInOrderedListCommand.key),
|
||||
'bullet-list': callCommand(wrapInBulletListCommand.key),
|
||||
}
|
||||
editor.action(actions[command])
|
||||
editorRoot.value?.querySelector<HTMLElement>('.ProseMirror')?.focus()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
crepe = new Crepe({
|
||||
root: editorRoot.value,
|
||||
defaultValue: props.initialContent,
|
||||
features: { [Crepe.Feature.TopBar]: false },
|
||||
featureConfigs: {
|
||||
[Crepe.Feature.Placeholder]: { text: '开始记录你的想法…' },
|
||||
[Crepe.Feature.CodeMirror]: {
|
||||
previewOnlyByDefault: true,
|
||||
previewLabel: 'Shiki 高亮预览',
|
||||
previewLoading: '正在高亮…',
|
||||
previewToggleText: (previewOnlyMode) => previewOnlyMode ? '编辑代码' : '查看高亮',
|
||||
renderPreview: (language, content, applyPreview) => {
|
||||
void highlightCode(content, language).then((html) => {
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = html
|
||||
applyPreview(template.content.firstElementChild as HTMLElement | null)
|
||||
})
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
crepe.on((listener) => {
|
||||
listener.markdownUpdated((_ctx, markdown, previousMarkdown) => {
|
||||
if (markdown === previousMarkdown || markdown === editorStore.content) return
|
||||
editorStore.updateContent(markdown)
|
||||
editorStore.scheduleAutoSave(settingsStore.autoSaveInterval)
|
||||
})
|
||||
})
|
||||
await crepe.create()
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => { void crepe?.destroy() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="visual-editor">
|
||||
<div class="markdown-toolbar" role="toolbar" aria-label="Markdown 格式工具栏">
|
||||
<button type="button" title="二级标题" @click="runCommand('heading')"><AppIcon :icon="CollectionTag" /><span>标题</span></button>
|
||||
<button type="button" title="加粗 (Ctrl+B)" @click="runCommand('bold')"><AppIcon :icon="Edit" /><span>加粗</span></button>
|
||||
<button type="button" title="斜体 (Ctrl+I)" @click="runCommand('italic')"><AppIcon :icon="EditPen" /><span>斜体</span></button>
|
||||
<span class="toolbar-divider" />
|
||||
<button type="button" title="有序列表" @click="runCommand('ordered-list')"><AppIcon :icon="Memo" /><span>有序列表</span></button>
|
||||
<button type="button" title="无序列表" @click="runCommand('bullet-list')"><AppIcon :icon="List" /><span>无序列表</span></button>
|
||||
</div>
|
||||
<div v-if="loading" class="editor-loading">正在加载编辑器…</div>
|
||||
<div ref="editorRoot" class="milkdown-host" :class="{ loading }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.visual-editor { display: flex; flex: 1; min-height: 0; flex-direction: column; background: var(--color-background-primary); }
|
||||
.markdown-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 2px; min-height: 42px; padding: 5px var(--space-lg); border-bottom: 1px solid var(--color-border-subtle); background: var(--color-surface-primary); }
|
||||
.markdown-toolbar button { display: inline-flex; align-items: center; gap: 5px; min-height: 30px; padding: 4px 8px; border-radius: var(--radius-sm); color: var(--color-text-secondary); }
|
||||
.markdown-toolbar button:hover { background: var(--color-background-hover); color: var(--color-text-primary); }
|
||||
.markdown-toolbar button:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: 1px; }
|
||||
.markdown-toolbar button span { font-size: var(--font-size-xs); }
|
||||
.toolbar-divider { width: 1px; height: 20px; margin: 0 var(--space-xs); background: var(--color-border-default); }
|
||||
.milkdown-host { flex: 1; min-height: 0; overflow: auto; color: var(--color-text-primary); }
|
||||
.milkdown-host.loading { visibility: hidden; }
|
||||
.editor-loading { padding: var(--space-xl); color: var(--color-text-tertiary); }
|
||||
.milkdown-host :deep(.milkdown) {
|
||||
min-height: 100%;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
--crepe-color-background: var(--color-background-primary);
|
||||
--crepe-color-on-background: var(--color-text-primary);
|
||||
--crepe-color-surface: var(--color-surface-primary);
|
||||
--crepe-color-surface-low: var(--color-background-secondary);
|
||||
--crepe-color-on-surface: var(--color-text-primary);
|
||||
--crepe-color-on-surface-variant: var(--color-text-secondary);
|
||||
--crepe-color-outline: var(--color-border-default);
|
||||
--crepe-color-primary: var(--color-accent-primary);
|
||||
--crepe-color-secondary: var(--color-accent-soft);
|
||||
--crepe-color-on-secondary: var(--color-text-primary);
|
||||
--crepe-color-inverse: var(--color-text-primary);
|
||||
--crepe-color-on-inverse: var(--color-background-primary);
|
||||
--crepe-color-inline-code: var(--color-error);
|
||||
--crepe-color-error: var(--color-error);
|
||||
--crepe-color-hover: var(--color-background-hover);
|
||||
--crepe-color-selected: var(--color-accent-soft);
|
||||
--crepe-color-inline-area: var(--color-background-tertiary);
|
||||
--crepe-font-default: var(--font-editor-sans);
|
||||
--crepe-font-code: var(--font-editor-mono);
|
||||
}
|
||||
.milkdown-host :deep(.ProseMirror) { box-sizing: border-box; width: min(100%, var(--editor-line-width, 80ch)); min-height: 100%; margin: 0 auto; padding: var(--space-3xl) var(--space-xl); outline: none; font-family: var(--font-editor-sans); font-size: var(--font-editor-size); line-height: var(--font-editor-line-height); caret-color: var(--color-accent-primary); }
|
||||
.milkdown-host :deep(.ProseMirror-selectednode) { outline-color: var(--color-accent-primary); }
|
||||
.milkdown-host :deep(code) { font-family: var(--font-editor-mono); }
|
||||
.milkdown-host :deep(.shiki) { box-sizing: border-box; width: 100%; overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); }
|
||||
:global([data-theme='dark']) .milkdown-host :deep(.shiki),
|
||||
:global([data-theme='dark']) .milkdown-host :deep(.shiki span) {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
:global([data-theme='dark']) .milkdown-host :deep(.milkdown) { color-scheme: dark; }
|
||||
@media (max-width: 680px) { .markdown-toolbar button span { display: none; } }
|
||||
</style>
|
||||
@@ -1,9 +1,51 @@
|
||||
import DOMPurify from 'dompurify'
|
||||
import { marked } from 'marked'
|
||||
import { createHighlighterCore } from 'shiki/core'
|
||||
import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'
|
||||
import css from '@shikijs/langs/css'
|
||||
import html from '@shikijs/langs/html'
|
||||
import javascript from '@shikijs/langs/javascript'
|
||||
import json from '@shikijs/langs/json'
|
||||
import markdown from '@shikijs/langs/markdown'
|
||||
import python from '@shikijs/langs/python'
|
||||
import shell from '@shikijs/langs/shellscript'
|
||||
import sql from '@shikijs/langs/sql'
|
||||
import typescript from '@shikijs/langs/typescript'
|
||||
import githubDark from '@shikijs/themes/github-dark'
|
||||
import githubLight from '@shikijs/themes/github-light'
|
||||
|
||||
marked.setOptions({ gfm: true, breaks: true })
|
||||
|
||||
export function renderMarkdown(source: string): string {
|
||||
const html = marked.parse(source, { async: false }) as string
|
||||
return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
|
||||
const highlighter = createHighlighterCore({
|
||||
themes: [githubLight, githubDark],
|
||||
langs: [markdown, html, css, javascript, typescript, json, python, shell, sql],
|
||||
engine: createJavaScriptRegexEngine(),
|
||||
})
|
||||
|
||||
const languageAliases: Record<string, string> = {
|
||||
bash: 'shell', js: 'javascript', md: 'markdown', plaintext: 'text', py: 'python', sh: 'shell', ts: 'typescript',
|
||||
}
|
||||
|
||||
export async function highlightCode(source: string, requestedLanguage = 'text'): Promise<string> {
|
||||
const shiki = await highlighter
|
||||
const language = languageAliases[requestedLanguage] ?? requestedLanguage
|
||||
const loadedLanguage = shiki.getLoadedLanguages().includes(language as never) ? language : 'markdown'
|
||||
return shiki.codeToHtml(source, {
|
||||
lang: loadedLanguage,
|
||||
themes: { light: 'github-light', dark: 'github-dark' },
|
||||
defaultColor: false,
|
||||
})
|
||||
}
|
||||
|
||||
export async function renderMarkdown(source: string): Promise<string> {
|
||||
const html = marked.parse(source, { async: false }) as string
|
||||
const documentNode = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html')
|
||||
for (const code of documentNode.querySelectorAll('pre > code')) {
|
||||
const requestedLanguage = [...code.classList].find((name) => name.startsWith('language-'))?.slice(9) || 'text'
|
||||
const highlighted = await highlightCode(code.textContent ?? '', requestedLanguage)
|
||||
const fragment = document.createRange().createContextualFragment(highlighted)
|
||||
code.parentElement?.replaceWith(fragment)
|
||||
}
|
||||
|
||||
return DOMPurify.sanitize(documentNode.body.innerHTML, { USE_PROFILES: { html: true } })
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "ESNext.Disposable", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
||||
Reference in New Issue
Block a user