feat: 发布v3.5版本
This commit is contained in:
@@ -6,7 +6,9 @@
|
|||||||
"Bash(npm run dev:*)",
|
"Bash(npm run dev:*)",
|
||||||
"Bash(taskkill:*)",
|
"Bash(taskkill:*)",
|
||||||
"Bash(timeout /t 5 /nobreak)",
|
"Bash(timeout /t 5 /nobreak)",
|
||||||
"Bash(git checkout:*)"
|
"Bash(git checkout:*)",
|
||||||
|
"Bash(npm install marked --save)",
|
||||||
|
"Bash(pnpm add marked)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<!-- 加载动画容器 -->
|
<!-- 加载动画容器 -->
|
||||||
<div id="yixinai-loader" class="loader-container">
|
<div id="yixinai-loader" class="loader-container">
|
||||||
<div class="loader-title">意心Ai 3.4</div>
|
<div class="loader-title">意心Ai 3.5</div>
|
||||||
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒,无需梯子</div>
|
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒,无需梯子</div>
|
||||||
<div class="loader-logo">
|
<div class="loader-logo">
|
||||||
<div class="pulse-box"></div>
|
<div class="pulse-box"></div>
|
||||||
|
|||||||
@@ -35,37 +35,33 @@
|
|||||||
"@floating-ui/dom": "^1.7.2",
|
"@floating-ui/dom": "^1.7.2",
|
||||||
"@floating-ui/vue": "^1.1.7",
|
"@floating-ui/vue": "^1.1.7",
|
||||||
"@jsonlee_12138/enum": "^1.0.4",
|
"@jsonlee_12138/enum": "^1.0.4",
|
||||||
|
"@shikijs/transformers": "^3.7.0",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.5.0",
|
||||||
"@vueuse/integrations": "^13.5.0",
|
"@vueuse/integrations": "^13.5.0",
|
||||||
|
"chatarea": "^6.0.3",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"driver.js": "^1.3.6",
|
"driver.js": "^1.3.6",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.10.4",
|
"element-plus": "^2.10.4",
|
||||||
"fingerprintjs": "^0.5.3",
|
"fingerprintjs": "^0.5.3",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"hook-fetch": "^2.0.4-beta.1",
|
"hook-fetch": "^2.0.4-beta.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mammoth": "^1.11.0",
|
"mammoth": "^1.11.0",
|
||||||
|
"marked": "^17.0.1",
|
||||||
|
"mermaid": "11.12.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdfjs-dist": "^5.4.449",
|
"pdfjs-dist": "^5.4.449",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"radash": "^12.1.1",
|
|
||||||
"reset-css": "^5.0.2",
|
|
||||||
"vue": "^3.5.17",
|
|
||||||
"vue-element-plus-x": "1.3.7",
|
|
||||||
"vue-router": "4",
|
|
||||||
"xlsx": "^0.18.5",
|
|
||||||
"@shikijs/transformers": "^3.7.0",
|
|
||||||
"chatarea": "^6.0.3",
|
|
||||||
"deepmerge": "^4.3.1",
|
|
||||||
"dompurify": "^3.2.6",
|
|
||||||
"github-markdown-css": "^5.8.1",
|
|
||||||
"highlight.js": "^11.11.1",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"mermaid": "11.12.0",
|
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"property-information": "^7.1.0",
|
"property-information": "^7.1.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"radash": "^12.1.1",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
@@ -74,46 +70,21 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.2",
|
"remark-rehype": "^11.1.2",
|
||||||
|
"reset-css": "^5.0.2",
|
||||||
"shiki": "^3.7.0",
|
"shiki": "^3.7.0",
|
||||||
"ts-md5": "^2.0.1",
|
"ts-md5": "^2.0.1",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unist-util-visit": "^5.0.0"
|
"unist-util-visit": "^5.0.0",
|
||||||
|
"vue": "^3.5.17",
|
||||||
|
"vue-element-plus-x": "1.3.7",
|
||||||
|
"vue-router": "4",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^4.16.2",
|
"@antfu/eslint-config": "^4.16.2",
|
||||||
"@changesets/cli": "^2.29.5",
|
"@changesets/cli": "^2.29.5",
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
|
||||||
"@types/fingerprintjs__fingerprintjs": "^3.0.2",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
|
||||||
"@vue/tsconfig": "^0.7.0",
|
|
||||||
"commitlint": "^19.8.1",
|
|
||||||
"cz-git": "^1.12.0",
|
|
||||||
"eslint": "^9.31.0",
|
|
||||||
"husky": "^9.1.7",
|
|
||||||
"lint-staged": "^16.1.2",
|
|
||||||
"postcss": "8.4.31",
|
|
||||||
"postcss-html": "1.5.0",
|
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"rollup-plugin-visualizer": "^6.0.3",
|
|
||||||
"sass-embedded": "^1.89.2",
|
|
||||||
"stylelint": "^16.21.1",
|
|
||||||
"stylelint-config-html": "^1.1.0",
|
|
||||||
"stylelint-config-recess-order": "^7.1.0",
|
|
||||||
"stylelint-config-recommended-scss": "^15.0.1",
|
|
||||||
"stylelint-config-recommended-vue": "^1.6.1",
|
|
||||||
"stylelint-config-standard": "^38.0.0",
|
|
||||||
"stylelint-config-standard-scss": "^15.0.1",
|
|
||||||
"typescript": "~5.8.3",
|
|
||||||
"typescript-api-pro": "^0.0.7",
|
|
||||||
"unocss": "66.3.3",
|
|
||||||
"unplugin-auto-import": "^19.3.0",
|
|
||||||
"unplugin-vue-components": "^28.8.0",
|
|
||||||
"vite": "^6.3.5",
|
|
||||||
"vite-plugin-compression": "^0.5.1",
|
|
||||||
"vite-plugin-env-typed": "^0.0.2",
|
|
||||||
"vite-plugin-svg-icons": "^2.0.1",
|
|
||||||
"vue-tsc": "^3.0.1",
|
|
||||||
"@chromatic-com/storybook": "^3.2.7",
|
"@chromatic-com/storybook": "^3.2.7",
|
||||||
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
"@jsonlee_12138/markdown-it-mermaid": "0.0.6",
|
"@jsonlee_12138/markdown-it-mermaid": "0.0.6",
|
||||||
"@storybook/addon-essentials": "^8.6.14",
|
"@storybook/addon-essentials": "^8.6.14",
|
||||||
"@storybook/addon-onboarding": "^8.6.14",
|
"@storybook/addon-onboarding": "^8.6.14",
|
||||||
@@ -127,22 +98,52 @@
|
|||||||
"@storybook/vue3": "^8.6.14",
|
"@storybook/vue3": "^8.6.14",
|
||||||
"@storybook/vue3-vite": "^8.6.14",
|
"@storybook/vue3-vite": "^8.6.14",
|
||||||
"@types/dom-speech-recognition": "^0.0.4",
|
"@types/dom-speech-recognition": "^0.0.4",
|
||||||
|
"@types/fingerprintjs__fingerprintjs": "^3.0.2",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^3.2.4",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"commitlint": "^19.8.1",
|
||||||
|
"cz-git": "^1.12.0",
|
||||||
|
"eslint": "^9.31.0",
|
||||||
"esno": "^4.8.0",
|
"esno": "^4.8.0",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^16.1.2",
|
||||||
"playwright": "^1.53.2",
|
"playwright": "^1.53.2",
|
||||||
|
"postcss": "8.4.31",
|
||||||
|
"postcss-html": "1.5.0",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
|
"sass-embedded": "^1.89.2",
|
||||||
"storybook": "^8.6.14",
|
"storybook": "^8.6.14",
|
||||||
"storybook-dark-mode": "^4.0.2",
|
"storybook-dark-mode": "^4.0.2",
|
||||||
|
"stylelint": "^16.21.1",
|
||||||
|
"stylelint-config-html": "^1.1.0",
|
||||||
|
"stylelint-config-recess-order": "^7.1.0",
|
||||||
|
"stylelint-config-recommended-scss": "^15.0.1",
|
||||||
|
"stylelint-config-recommended-vue": "^1.6.1",
|
||||||
|
"stylelint-config-standard": "^38.0.0",
|
||||||
|
"stylelint-config-standard-scss": "^15.0.1",
|
||||||
"terser": "^5.43.1",
|
"terser": "^5.43.1",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-api-pro": "^0.0.7",
|
||||||
|
"unocss": "66.3.3",
|
||||||
|
"unplugin-auto-import": "^19.3.0",
|
||||||
|
"unplugin-vue-components": "^28.8.0",
|
||||||
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dts": "^4.5.4",
|
"vite-plugin-dts": "^4.5.4",
|
||||||
|
"vite-plugin-env-typed": "^0.0.2",
|
||||||
"vite-plugin-lib-inject-css": "^2.2.2",
|
"vite-plugin-lib-inject-css": "^2.2.2",
|
||||||
"vitest": "^3.2.4"
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vitest": "^3.2.4",
|
||||||
|
"vue-tsc": "^3.0.1"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
|
|||||||
350
Yi.Ai.Vue3/pnpm-lock.yaml
generated
350
Yi.Ai.Vue3/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
897
Yi.Ai.Vue3/src/components/MarkedMarkdown/index.vue
Normal file
897
Yi.Ai.Vue3/src/components/MarkedMarkdown/index.vue
Normal file
@@ -0,0 +1,897 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { Marked } from 'marked';
|
||||||
|
import hljs from 'highlight.js';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { ElDrawer } from 'element-plus';
|
||||||
|
import { useDesignStore } from '@/stores';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
theme?: 'light' | 'dark' | 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
content: '',
|
||||||
|
theme: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const designStore = useDesignStore();
|
||||||
|
const { darkMode } = storeToRefs(designStore);
|
||||||
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
const renderedHtml = ref('');
|
||||||
|
|
||||||
|
// 抽屉相关状态
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const previewHtml = ref('');
|
||||||
|
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
||||||
|
|
||||||
|
// 创建 marked 实例
|
||||||
|
const marked = new Marked();
|
||||||
|
|
||||||
|
// 配置 marked
|
||||||
|
marked.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自定义渲染器
|
||||||
|
const renderer = {
|
||||||
|
// 代码块渲染
|
||||||
|
code(token: { text: string; lang?: string }) {
|
||||||
|
const { text, lang } = token;
|
||||||
|
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||||
|
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
|
||||||
|
|
||||||
|
let highlighted: string;
|
||||||
|
try {
|
||||||
|
highlighted = hljs.highlight(text, { language: validLanguage, ignoreIllegals: true }).value;
|
||||||
|
} catch {
|
||||||
|
highlighted = hljs.highlightAuto(text).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const langLabel = lang || 'code';
|
||||||
|
|
||||||
|
// 生成行号
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const lineNumbers = lines.map((_, i) => `<span class="line-number">${i + 1}</span>`).join('');
|
||||||
|
|
||||||
|
// 判断是否为HTML代码
|
||||||
|
const isHtml = lang?.toLowerCase() === 'html' || lang?.toLowerCase() === 'htm';
|
||||||
|
|
||||||
|
// 预览按钮(仅HTML显示)
|
||||||
|
const previewBtn = isHtml ? `
|
||||||
|
<button class="preview-btn" data-html="${encodeURIComponent(text)}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
<span>预览</span>
|
||||||
|
</button>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
return `<div class="code-block-wrapper${isHtml ? ' is-html' : ''}">
|
||||||
|
<div class="code-block-header">
|
||||||
|
<span class="code-lang">${langLabel}</span>
|
||||||
|
<div class="code-block-actions">
|
||||||
|
${previewBtn}
|
||||||
|
<button class="copy-btn" data-code="${encodeURIComponent(text)}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
<span>复制</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-block-body">
|
||||||
|
<div class="line-numbers">${lineNumbers}</div>
|
||||||
|
<pre class="hljs"><code class="language-${validLanguage}">${highlighted}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 行内代码
|
||||||
|
codespan(token: { text: string }) {
|
||||||
|
return `<code class="inline-code">${token.text}</code>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 链接
|
||||||
|
link(token: { href: string; title?: string; text: string }) {
|
||||||
|
const { href, title, text } = token;
|
||||||
|
const titleAttr = title ? ` title="${title}"` : '';
|
||||||
|
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
image(token: { href: string; title?: string; text: string }) {
|
||||||
|
const { href, title, text } = token;
|
||||||
|
const titleAttr = title ? ` title="${title}"` : '';
|
||||||
|
return `<img src="${href}" alt="${text}"${titleAttr} loading="lazy" class="markdown-image" />`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 表格
|
||||||
|
table(token: { header: string; body: string }) {
|
||||||
|
return `<div class="table-wrapper"><table>${token.header}${token.body}</table></div>`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
marked.use({ renderer });
|
||||||
|
|
||||||
|
// 防抖渲染,优化流式性能
|
||||||
|
let renderTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let lastContent = '';
|
||||||
|
const RENDER_DELAY = 16; // 约60fps
|
||||||
|
|
||||||
|
function scheduleRender(content: string) {
|
||||||
|
if (renderTimer) {
|
||||||
|
clearTimeout(renderTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTimer = setTimeout(() => {
|
||||||
|
renderContent(content);
|
||||||
|
renderTimer = null;
|
||||||
|
}, RENDER_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染内容
|
||||||
|
async function renderContent(content: string) {
|
||||||
|
if (!content) {
|
||||||
|
renderedHtml.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果内容没变化,跳过渲染
|
||||||
|
if (content === lastContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastContent = content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawHtml = await marked.parse(content);
|
||||||
|
// 使用 DOMPurify 清理 HTML,防止 XSS
|
||||||
|
renderedHtml.value = DOMPurify.sanitize(rawHtml, {
|
||||||
|
ADD_TAGS: ['iframe'],
|
||||||
|
ADD_ATTR: ['target', 'data-code', 'data-html'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染后绑定按钮事件
|
||||||
|
nextTick(() => {
|
||||||
|
bindButtons();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Markdown 渲染错误:', error);
|
||||||
|
renderedHtml.value = `<pre>${content}</pre>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定按钮事件
|
||||||
|
function bindButtons() {
|
||||||
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
|
// 绑定复制按钮
|
||||||
|
const copyButtons = containerRef.value.querySelectorAll('.copy-btn');
|
||||||
|
copyButtons.forEach((btn) => {
|
||||||
|
const newBtn = btn.cloneNode(true) as HTMLElement;
|
||||||
|
btn.parentNode?.replaceChild(newBtn, btn);
|
||||||
|
|
||||||
|
newBtn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const code = decodeURIComponent(newBtn.getAttribute('data-code') || '');
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
const spanEl = newBtn.querySelector('span');
|
||||||
|
if (spanEl) {
|
||||||
|
const originalText = spanEl.textContent;
|
||||||
|
spanEl.textContent = '已复制';
|
||||||
|
newBtn.classList.add('copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
spanEl.textContent = originalText;
|
||||||
|
newBtn.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绑定预览按钮
|
||||||
|
const previewButtons = containerRef.value.querySelectorAll('.preview-btn');
|
||||||
|
previewButtons.forEach((btn) => {
|
||||||
|
const newBtn = btn.cloneNode(true) as HTMLElement;
|
||||||
|
btn.parentNode?.replaceChild(newBtn, btn);
|
||||||
|
|
||||||
|
newBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const html = decodeURIComponent(newBtn.getAttribute('data-html') || '');
|
||||||
|
openHtmlPreview(html);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开HTML预览抽屉
|
||||||
|
function openHtmlPreview(html: string) {
|
||||||
|
// 检查是否是完整的HTML文档,如果不是则包装一下
|
||||||
|
let fullHtml = html;
|
||||||
|
if (!html.toLowerCase().includes('<!doctype') && !html.toLowerCase().includes('<html')) {
|
||||||
|
fullHtml = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>* { box-sizing: border-box; } body { margin: 0; padding: 16px; font-family: sans-serif; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${html}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
previewHtml.value = fullHtml;
|
||||||
|
drawerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
const resolvedTheme = computed(() => {
|
||||||
|
if (props.theme === 'auto') {
|
||||||
|
// 从全局状态获取主题
|
||||||
|
return darkMode.value === 'light' ? 'light' : 'dark';
|
||||||
|
}
|
||||||
|
return props.theme;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听内容变化
|
||||||
|
watch(
|
||||||
|
() => props.content,
|
||||||
|
(newContent) => {
|
||||||
|
scheduleRender(newContent);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 暴露方法供外部调用
|
||||||
|
defineExpose({
|
||||||
|
refresh: () => renderContent(props.content),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (renderTimer) {
|
||||||
|
clearTimeout(renderTimer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
class="marked-markdown"
|
||||||
|
:class="[`theme-${resolvedTheme}`]"
|
||||||
|
v-html="renderedHtml"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- HTML预览抽屉 -->
|
||||||
|
<ElDrawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
title="HTML 预览"
|
||||||
|
direction="rtl"
|
||||||
|
size="50%"
|
||||||
|
:append-to-body="true"
|
||||||
|
class="html-preview-drawer"
|
||||||
|
>
|
||||||
|
<div class="preview-container">
|
||||||
|
<iframe
|
||||||
|
ref="iframeRef"
|
||||||
|
class="preview-iframe"
|
||||||
|
:srcdoc="previewHtml"
|
||||||
|
sandbox="allow-scripts allow-modals allow-forms allow-popups allow-same-origin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.marked-markdown {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
// 深色主题
|
||||||
|
&.theme-dark {
|
||||||
|
color: #e6edf3;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #58a6ff;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #e6edf3;
|
||||||
|
border-bottom-color: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
background-color: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
color: #8b949e;
|
||||||
|
border-left-color: #3b434b;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.inline-code {
|
||||||
|
background-color: rgba(110, 118, 129, 0.4);
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-wrapper {
|
||||||
|
background-color: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
|
||||||
|
.code-block-header {
|
||||||
|
background-color: #21262d;
|
||||||
|
border-bottom-color: #30363d;
|
||||||
|
|
||||||
|
.code-lang {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn,
|
||||||
|
.preview-btn {
|
||||||
|
color: #8b949e;
|
||||||
|
&:hover {
|
||||||
|
color: #e6edf3;
|
||||||
|
background-color: #30363d;
|
||||||
|
}
|
||||||
|
&.copied {
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-body {
|
||||||
|
.line-numbers {
|
||||||
|
background-color: #161b22;
|
||||||
|
border-right: 1px solid #30363d;
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
color: #6e7681;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.hljs {
|
||||||
|
background-color: #161b22;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
th {
|
||||||
|
background-color: #21262d;
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
border-color: #30363d;
|
||||||
|
}
|
||||||
|
tr:nth-child(2n) {
|
||||||
|
background-color: #161b22;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-image {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 浅色主题
|
||||||
|
&.theme-light {
|
||||||
|
color: #24292f;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0969da;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #24292f;
|
||||||
|
border-bottom-color: #d0d7de;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
background-color: #d0d7de;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
color: #57606a;
|
||||||
|
border-left-color: #d0d7de;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.inline-code {
|
||||||
|
background-color: rgba(175, 184, 193, 0.2);
|
||||||
|
color: #24292f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-wrapper {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
|
||||||
|
.code-block-header {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border-bottom-color: #d0d7de;
|
||||||
|
|
||||||
|
.code-lang {
|
||||||
|
color: #57606a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn,
|
||||||
|
.preview-btn {
|
||||||
|
color: #57606a;
|
||||||
|
&:hover {
|
||||||
|
color: #24292f;
|
||||||
|
background-color: #d0d7de;
|
||||||
|
}
|
||||||
|
&.copied {
|
||||||
|
color: #1a7f37;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-body {
|
||||||
|
.line-numbers {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border-right: 1px solid #d0d7de;
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
color: #8c959f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.hljs {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
th {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
border-color: #d0d7de;
|
||||||
|
}
|
||||||
|
tr:nth-child(2n) {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用样式
|
||||||
|
> *:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:last-child {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 段落
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 2em;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li + li {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
border-left: 0.25em solid;
|
||||||
|
|
||||||
|
> :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割线
|
||||||
|
hr {
|
||||||
|
height: 0.25em;
|
||||||
|
padding: 0;
|
||||||
|
margin: 24px 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行内代码
|
||||||
|
code.inline-code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 85%;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代码块
|
||||||
|
.code-block-wrapper {
|
||||||
|
margin: 16px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.code-block-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
|
||||||
|
.code-lang {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn,
|
||||||
|
.preview-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-body {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
// 自定义滚动条样式
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(128, 128, 128, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(128, 128, 128, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px 0;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
padding: 0 12px;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
//line-height: 21px;
|
||||||
|
height: 22.72px;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.hljs {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
padding-left: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 21px;
|
||||||
|
flex: 1;
|
||||||
|
white-space: pre;
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
.markdown-image {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强调
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除线
|
||||||
|
del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务列表
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlight.js 深色主题样式
|
||||||
|
.marked-markdown.theme-dark {
|
||||||
|
.hljs {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #8b949e;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-addition {
|
||||||
|
color: #ff7b72;
|
||||||
|
}
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-meta .hljs-meta-string,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-regexp {
|
||||||
|
color: #a5d6ff;
|
||||||
|
}
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #d2a8ff;
|
||||||
|
}
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-class .hljs-title,
|
||||||
|
.hljs-type {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-subst,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-meta .hljs-keyword,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-link {
|
||||||
|
color: #ffa657;
|
||||||
|
}
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #ffa198;
|
||||||
|
}
|
||||||
|
.hljs-formula {
|
||||||
|
background-color: #21262d;
|
||||||
|
}
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlight.js 浅色主题样式
|
||||||
|
.marked-markdown.theme-light {
|
||||||
|
.hljs {
|
||||||
|
color: #24292f;
|
||||||
|
}
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #6e7781;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-addition {
|
||||||
|
color: #cf222e;
|
||||||
|
}
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-meta .hljs-meta-string,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-regexp {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #8250df;
|
||||||
|
}
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-class .hljs-title,
|
||||||
|
.hljs-type {
|
||||||
|
color: #0550ae;
|
||||||
|
}
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-subst,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-meta .hljs-keyword,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-link {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #82071e;
|
||||||
|
}
|
||||||
|
.hljs-formula {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML预览抽屉样式
|
||||||
|
.html-preview-drawer {
|
||||||
|
.el-drawer__header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.marked-markdown .code-block-wrapper pre.hljs {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(128, 128, 128, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(128, 128, 128, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,7 +12,7 @@ import { getSelectableTokenInfo } from '@/api';
|
|||||||
import { useUserStore } from '@/stores/modules/user';
|
import { useUserStore } from '@/stores/modules/user';
|
||||||
import { useAgentSessionStore } from '@/stores/modules/agentSession';
|
import { useAgentSessionStore } from '@/stores/modules/agentSession';
|
||||||
import { getUserProfilePicture } from '@/utils/user.ts';
|
import { getUserProfilePicture } from '@/utils/user.ts';
|
||||||
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
|
import MarkedMarkdown from '@/components/MarkedMarkdown/index.vue';
|
||||||
import agentAvatar from '@/assets/images/czld.png';
|
import agentAvatar from '@/assets/images/czld.png';
|
||||||
import '@/styles/github-markdown.css';
|
import '@/styles/github-markdown.css';
|
||||||
import '@/styles/yixin-markdown.scss';
|
import '@/styles/yixin-markdown.scss';
|
||||||
@@ -547,12 +547,10 @@ function cancelSSE() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content="{ item }">
|
<template #content="{ item }">
|
||||||
<YMarkdown
|
<MarkedMarkdown
|
||||||
v-if="item.content && (item.role === 'assistant' || item.role === 'system')"
|
v-if="item.content && (item.role === 'assistant' || item.role === 'system')"
|
||||||
class="markdown-body"
|
class="markdown-body"
|
||||||
:markdown="item.content"
|
:content="item.content"
|
||||||
:themes="{ light: 'github-light', dark: 'github-dark' }"
|
|
||||||
default-theme-mode="dark"
|
|
||||||
/>
|
/>
|
||||||
<div v-if="item.role === 'user'" class="user-content">
|
<div v-if="item.role === 'user'" class="user-content">
|
||||||
{{ item.content }}
|
{{ item.content }}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { useModelStore } from '@/stores/modules/model';
|
|||||||
import { useSessionStore } from '@/stores/modules/session';
|
import { useSessionStore } from '@/stores/modules/session';
|
||||||
import { useUserStore } from '@/stores/modules/user';
|
import { useUserStore } from '@/stores/modules/user';
|
||||||
import { getUserProfilePicture, systemProfilePicture } from '@/utils/user.ts';
|
import { getUserProfilePicture, systemProfilePicture } from '@/utils/user.ts';
|
||||||
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
|
import MarkedMarkdown from '@/components/MarkedMarkdown/index.vue';
|
||||||
import '@/styles/github-markdown.css';
|
import '@/styles/github-markdown.css';
|
||||||
import '@/styles/yixin-markdown.scss';
|
import '@/styles/yixin-markdown.scss';
|
||||||
|
|
||||||
@@ -202,6 +202,13 @@ function handleDataChunk(chunk: AnyObject) {
|
|||||||
latest.content += parsed.content;
|
latest.content += parsed.content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 流式输出时保持滚动条在底部(等待 DOM 更新后再滚动)
|
||||||
|
nextTick(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
bubbleListRef.value?.scrollToBottom();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('解析数据时出错:', err);
|
console.error('解析数据时出错:', err);
|
||||||
@@ -864,7 +871,7 @@ onUnmounted(() => {
|
|||||||
<!-- 自定义气泡内容 -->
|
<!-- 自定义气泡内容 -->
|
||||||
<template #content="{ item }">
|
<template #content="{ item }">
|
||||||
<!-- chat 内容走 markdown -->
|
<!-- chat 内容走 markdown -->
|
||||||
<YMarkdown v-if="item.content && (item.role === 'assistant' || item.role === 'system')" class="markdown-body" :markdown="item.content" :themes="{ light: 'github-light', dark: 'github-dark' }" default-theme-mode="dark" />
|
<MarkedMarkdown v-if="item.content && (item.role === 'assistant' || item.role === 'system')" class="markdown-body" :content="item.content" />
|
||||||
<!-- user 内容 纯文本 + 图片 + 文件 -->
|
<!-- user 内容 纯文本 + 图片 + 文件 -->
|
||||||
<div v-if="item.role === 'user'" class="user-content-wrapper">
|
<div v-if="item.role === 'user'" class="user-content-wrapper">
|
||||||
<!-- 图片列表 -->
|
<!-- 图片列表 -->
|
||||||
|
|||||||
@@ -1794,6 +1794,18 @@
|
|||||||
.model-logo{
|
.model-logo{
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
.marked-markdown.theme-light .code-block-wrapper .code-block-header{
|
||||||
|
@include dark-theme-div;
|
||||||
|
}
|
||||||
|
.marked-markdown.theme-light .code-block-wrapper{
|
||||||
|
@include dark-theme-div;
|
||||||
|
}
|
||||||
|
.marked-markdown.theme-light .code-block-wrapper .code-block-header .code-lang{
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.marked-markdown.theme-light .code-block-wrapper .code-block-body .line-numbers{
|
||||||
|
@include dark-theme-div;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,705 +0,0 @@
|
|||||||
import type { BubbleProps } from '@components/Bubble/types';
|
|
||||||
import type { BubbleListProps } from '@components/BubbleList/types';
|
|
||||||
import type { FilesType } from '@components/FilesCard/types';
|
|
||||||
|
|
||||||
import type { ThinkingStatus } from '@components/Thinking/types';
|
|
||||||
|
|
||||||
// 头像1
|
|
||||||
export const avatar1: string =
|
|
||||||
'https://avatars.githubusercontent.com/u/76239030?v=4';
|
|
||||||
|
|
||||||
// 头像2
|
|
||||||
export const avatar2: string =
|
|
||||||
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png';
|
|
||||||
|
|
||||||
// md 普通内容
|
|
||||||
export const mdContent = `
|
|
||||||
### 行内公式
|
|
||||||
1. 欧拉公式:$e^{i\\pi} + 1 = 0$
|
|
||||||
2. 二次方程求根公式:$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$
|
|
||||||
3. 向量点积:$\\vec{a} \\cdot \\vec{b} = a_x b_x + a_y b_y + a_z b_z$
|
|
||||||
### []包裹公式
|
|
||||||
\\[ e^{i\\pi} + 1 = 0 \\]
|
|
||||||
|
|
||||||
\\[\\boxed{boxed包裹}\\]
|
|
||||||
|
|
||||||
### 块级公式
|
|
||||||
1. 傅里叶变换:
|
|
||||||
$$
|
|
||||||
F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt
|
|
||||||
$$
|
|
||||||
|
|
||||||
2. 矩阵乘法:
|
|
||||||
$$
|
|
||||||
\\begin{bmatrix}
|
|
||||||
a & b \\\\
|
|
||||||
c & d
|
|
||||||
\\end{bmatrix}
|
|
||||||
\\begin{bmatrix}
|
|
||||||
x \\\\
|
|
||||||
y
|
|
||||||
\\end{bmatrix}
|
|
||||||
=
|
|
||||||
\\begin{bmatrix}
|
|
||||||
ax + by \\\\
|
|
||||||
cx + dy
|
|
||||||
\\end{bmatrix}
|
|
||||||
$$
|
|
||||||
|
|
||||||
3. 泰勒级数展开:
|
|
||||||
$$
|
|
||||||
f(x) = \\sum_{n=0}^{\\infty} \\frac{f^{(n)}(a)}{n!} (x - a)^n
|
|
||||||
$$
|
|
||||||
|
|
||||||
4. 拉普拉斯方程:
|
|
||||||
$$
|
|
||||||
\\nabla^2 u = \\frac{\\partial^2 u}{\\partial x^2} + \\frac{\\partial^2 u}{\\partial y^2} + \\frac{\\partial^2 u}{\\partial z^2} = 0
|
|
||||||
$$
|
|
||||||
|
|
||||||
5. 概率密度函数(正态分布):
|
|
||||||
$$
|
|
||||||
f(x) = \\frac{1}{\\sqrt{2\\pi\\sigma^2}} e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}
|
|
||||||
$$
|
|
||||||
|
|
||||||
# 标题
|
|
||||||
这是一个 Markdown 示例。
|
|
||||||
- 列表项 1
|
|
||||||
- 列表项 2
|
|
||||||
**粗体文本** 和 *斜体文本*
|
|
||||||
|
|
||||||
- [x] Add some task
|
|
||||||
- [ ] Do some task
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
// md 代码块高亮
|
|
||||||
export const highlightMdContent = `
|
|
||||||
#### 切换右侧的secureViewCode进行安全预览或者不启用安全预览模式下 会呈现不同的网页预览效果
|
|
||||||
##### 通过enableCodeLineNumber属性开启代码行号
|
|
||||||
\`\`\`html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>炫酷文字动效</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; overflow: hidden; }
|
|
||||||
canvas { display: block; }
|
|
||||||
.text-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
text-align: center;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: clamp(2rem, 8vw, 5rem);
|
|
||||||
margin: 0;
|
|
||||||
color: white;
|
|
||||||
text-shadow: 0 0 10px rgba(0,0,0,0.3);
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeIn 3s forwards 0.5s;
|
|
||||||
}
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<canvas id="canvas"></canvas>
|
|
||||||
<div class="text-container">
|
|
||||||
<h1 id="main-text">AWESOME TEXT</h1>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const canvas = document.getElementById('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
canvas.width = window.innerWidth;
|
|
||||||
canvas.height = window.innerHeight;
|
|
||||||
const text = document.getElementById('main-text');
|
|
||||||
|
|
||||||
class Particle {
|
|
||||||
constructor() {
|
|
||||||
this.x = Math.random() * canvas.width;
|
|
||||||
this.y = Math.random() * canvas.height;
|
|
||||||
this.size = Math.random() * 3 + 1;
|
|
||||||
this.speedX = Math.random() * 3 - 1.5;
|
|
||||||
this.speedY = Math.random() * 3 - 1.5;
|
|
||||||
this.color = \`hsl(\${Math.random() * 360}, 70%, 60%)\`;
|
|
||||||
}
|
|
||||||
update() {
|
|
||||||
this.x += this.speedX;
|
|
||||||
this.y += this.speedY;
|
|
||||||
if (this.size > 0.2) this.size -= 0.01;
|
|
||||||
}
|
|
||||||
draw() {
|
|
||||||
ctx.fillStyle = this.color;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let particles = [];
|
|
||||||
function init() {
|
|
||||||
particles = [];
|
|
||||||
for (let i = 0; i < 200; i++) {
|
|
||||||
particles.push(new Particle());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
for (let i = 0; i < particles.length; i++) {
|
|
||||||
particles[i].update();
|
|
||||||
particles[i].draw();
|
|
||||||
for (let j = i; j < particles.length; j++) {
|
|
||||||
const dx = particles[i].x - particles[j].x;
|
|
||||||
const dy = particles[i].y - particles[j].y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance < 100) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.strokeStyle = \`rgba(255,255,255,\${0.1 - distance/1000})\`;
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.moveTo(particles[i].x, particles[i].y);
|
|
||||||
ctx.lineTo(particles[j].x, particles[j].y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
animate();
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
canvas.width = window.innerWidth;
|
|
||||||
canvas.height = window.innerHeight;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 自定义文字功能
|
|
||||||
text.addEventListener('click', () => {
|
|
||||||
const newText = prompt('输入新文字:', text.textContent);
|
|
||||||
if (newText) {
|
|
||||||
text.textContent = newText;
|
|
||||||
text.style.opacity = 0;
|
|
||||||
setTimeout(() => {
|
|
||||||
text.style.opacity = 1;
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
\`\`\`
|
|
||||||
\`\`\`html
|
|
||||||
<div class="product-card">
|
|
||||||
<div class="badge">新品</div>
|
|
||||||
<img src="https://picsum.photos/300/200?product" alt="产品图片">
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<h3>无线蓝牙耳机 Pro</h3>
|
|
||||||
<p class="description">主动降噪技术,30小时续航,IPX5防水等级</p>
|
|
||||||
|
|
||||||
<div class="rating">
|
|
||||||
<span>★★★★☆</span>
|
|
||||||
<span class="reviews">(124条评价)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="price-container">
|
|
||||||
<span class="price">¥499</span>
|
|
||||||
<span class="original-price">¥699</span>
|
|
||||||
<span class="discount">7折</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button class="cart-btn">加入购物车</button>
|
|
||||||
<button class="fav-btn">❤️</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="meta">
|
|
||||||
<span>✓ 次日达</span>
|
|
||||||
<span>✓ 7天无理由</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.product-card {
|
|
||||||
width: 280px;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
position: relative;
|
|
||||||
background: white;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
background: #ff6b6b;
|
|
||||||
color: white;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 180px;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 8px 0;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 8px 0 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 10px 0;
|
|
||||||
color: #ffb300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reviews {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #888;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #ff4757;
|
|
||||||
}
|
|
||||||
|
|
||||||
.original-price {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discount {
|
|
||||||
background: #fff200;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 16px 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-btn {
|
|
||||||
flex: 1;
|
|
||||||
background: #5352ed;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-btn:hover {
|
|
||||||
background: #3742fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fav-btn {
|
|
||||||
width: 42px;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fav-btn:hover {
|
|
||||||
border-color: #ff6b6b;
|
|
||||||
color: #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #2ed573;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
\`\`\`
|
|
||||||
###### 非\`commonMark\`语法,dom多个
|
|
||||||
<pre>
|
|
||||||
<code class="language-java">
|
|
||||||
public class HelloWorld {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println("Hello, world!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
\`\`\`echarts
|
|
||||||
use codeXRender for echarts render
|
|
||||||
\`\`\`
|
|
||||||
### javascript
|
|
||||||
\`\`\`javascript
|
|
||||||
console.log('Hello, world!');
|
|
||||||
\`\`\`
|
|
||||||
### java
|
|
||||||
\`\`\`java
|
|
||||||
public class HelloWorld {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println("Hello, world!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
\`\`\`typescript
|
|
||||||
import {
|
|
||||||
ArrowDownBold,
|
|
||||||
CopyDocument,
|
|
||||||
Moon,
|
|
||||||
Sunny
|
|
||||||
} from '@element-plus/icons-vue';
|
|
||||||
import { ElButton, ElSpace } from 'element-plus';
|
|
||||||
import { h } from 'vue';
|
|
||||||
|
|
||||||
/* ----------------------------------- 按钮组 ---------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 language标签
|
|
||||||
* @date 2025-06-25 17:48:15
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param language
|
|
||||||
*/
|
|
||||||
export function languageEle(language: string) {
|
|
||||||
return h(
|
|
||||||
ElSpace,
|
|
||||||
{},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
// md 美人鱼图表
|
|
||||||
export const mermaidMdContent = `
|
|
||||||
|
|
||||||
### mermaid 饼状图
|
|
||||||
\`\`\`mermaid
|
|
||||||
pie
|
|
||||||
"传媒及文化相关" : 35
|
|
||||||
"广告与市场营销" : 8
|
|
||||||
"游戏开发" : 15
|
|
||||||
"影视动画与特效" : 12
|
|
||||||
"互联网产品设计" : 10
|
|
||||||
"VR/AR开发" : 5
|
|
||||||
"其他" : 15
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// md 数学公式
|
|
||||||
export const mathMdContent = `
|
|
||||||
### mermaid 流程图
|
|
||||||
\`\`\`mermaid
|
|
||||||
graph LR
|
|
||||||
1 --> 2
|
|
||||||
2 --> 3
|
|
||||||
3 --> 4
|
|
||||||
2 --> 1
|
|
||||||
2-3 --> 1-3
|
|
||||||
\`\`\`
|
|
||||||
\`\`\`mermaid
|
|
||||||
flowchart TD
|
|
||||||
Start[开始] --> Check[是否通过?]
|
|
||||||
Check -- 是 --> Pass[流程继续]
|
|
||||||
Check -- 否 --> Reject[流程结束]
|
|
||||||
\`\`\`
|
|
||||||
\`\`\`mermaid
|
|
||||||
flowchart TD
|
|
||||||
%% 前端专项四层结构
|
|
||||||
A["战略层
|
|
||||||
【提升用户体验】"]
|
|
||||||
--> B["架构层
|
|
||||||
【微前端方案选型】"]
|
|
||||||
--> C["框架层
|
|
||||||
【React+TS技术栈】"]
|
|
||||||
--> D["实现层
|
|
||||||
【组件库开发】"]
|
|
||||||
style A fill:#FFD700,stroke:#FFA500
|
|
||||||
style B fill:#87CEFA,stroke:#1E90FF
|
|
||||||
style C fill:#9370DB,stroke:#663399
|
|
||||||
style D fill:#FF6347,stroke:#CD5C5C
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
### mermaid 数学公式
|
|
||||||
\`\`\`mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
autonumber
|
|
||||||
participant 1 as $$alpha$$
|
|
||||||
participant 2 as $$beta$$
|
|
||||||
1->>2: Solve: $$\sqrt{2+2}$$
|
|
||||||
2-->>1: Answer: $$2$$
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
`;
|
|
||||||
export const customAttrContent = `
|
|
||||||
<a href="https://element-plus-x.com/">element-plus-x</a>
|
|
||||||
<h1>标题1</h1>
|
|
||||||
<h2>标题2</h2>
|
|
||||||
`;
|
|
||||||
export type MessageItem = BubbleProps & {
|
|
||||||
key: number;
|
|
||||||
role: 'ai' | 'user' | 'system';
|
|
||||||
avatar: string;
|
|
||||||
thinkingStatus?: ThinkingStatus;
|
|
||||||
expanded?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// md 复杂图表
|
|
||||||
export const mermaidComplexMdContent = `
|
|
||||||
### Mermaid 渲染复杂图表案例
|
|
||||||
\`\`\`mermaid
|
|
||||||
graph LR
|
|
||||||
A[用户] -->|请求交互| B[前端应用]
|
|
||||||
B -->|API调用| C[API网关]
|
|
||||||
C -->|认证请求| D[认证服务]
|
|
||||||
C -->|业务请求| E[业务服务]
|
|
||||||
E -->|数据读写| F[数据库]
|
|
||||||
E -->|缓存操作| G[缓存服务]
|
|
||||||
E -->|消息发布| H[消息队列]
|
|
||||||
H -->|触发任务| I[后台任务]
|
|
||||||
|
|
||||||
subgraph "微服务集群"
|
|
||||||
D[认证服务]
|
|
||||||
E[业务服务]
|
|
||||||
I[后台任务]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "数据持久层"
|
|
||||||
F[数据库]
|
|
||||||
G[缓存服务]
|
|
||||||
end
|
|
||||||
|
|
||||||
`;
|
|
||||||
// animateTestMdContent 为动画测试的 markdown 内容,包含唐代王勃《滕王阁序》并做了格式优化
|
|
||||||
// animateTestMdContent 为动画测试的 markdown 内容,包含唐代王勃《滕王阁序》并做了格式优化(部分内容采用表格样式展示)
|
|
||||||
export const animateTestMdContent = `
|
|
||||||
### 唐代:王勃《滕王阁序》
|
|
||||||
|
|
||||||
| 章节 | 内容 |
|
|
||||||
| ---- | ---- |
|
|
||||||
| 开篇 | 豫章故郡,洪都新府。<br>星分翼轸,地接衡庐。<br>襟三江而带五湖,控蛮荆而引瓯越。<br>物华天宝,龙光射牛斗之墟;人杰地灵,徐孺下陈蕃之榻。<br>雄州雾列,俊采星驰。台隍枕夷夏之交,宾主尽东南之美。<br>都督阎公之雅望,棨戟遥临;宇文新州之懿范,襜帷暂驻。<br>十旬休假,胜友如云;千里逢迎,高朋满座。<br>腾蛟起凤,孟学士之词宗;紫电青霜,王将军之武库。<br>家君作宰,路出名区;童子何知,躬逢胜饯。 |
|
|
||||||
| 九月三秋 | 时维九月,序属三秋。<br>潦水尽而寒潭清,烟光凝而暮山紫。<br>俨骖騑于上路,访风景于崇阿。<br>临帝子之长洲,得天人之旧馆。<br>层峦耸翠,上出重霄;飞阁流丹,下临无地。<br>鹤汀凫渚,穷岛屿之萦回;桂殿兰宫,即冈峦之体势。 |
|
|
||||||
| 山川景色 | 披绣闼,俯雕甍,山原旷其盈视,川泽纡其骇瞩。<br>闾阎扑地,钟鸣鼎食之家;舸舰迷津,青雀黄龙之舳。<br>云销雨霁,彩彻区明。落霞与孤鹜齐飞,秋水共长天一色。<br>渔舟唱晚,响穷彭蠡之滨,雁阵惊寒,声断衡阳之浦。 |
|
|
||||||
| 兴致抒怀 | 遥襟甫畅,逸兴遄飞。爽籁发而清风生,纤歌凝而白云遏。<br>睢园绿竹,气凌彭泽之樽;邺水朱华,光照临川之笔。<br>四美具,二难并。穷睇眄于中天,极娱游于暇日。<br>天高地迥,觉宇宙之无穷;兴尽悲来,识盈虚之有数。<br>望长安于日下,目吴会于云间。地势极而南溟深,天柱高而北辰远。<br>关山难越,谁悲失路之人;萍水相逢,尽是他乡之客。<br>怀帝阍而不见,奉宣室以何年? |
|
|
||||||
| 感慨身世 | 嗟乎!时运不齐,命途多舛。<br>冯唐易老,李广难封。<br>屈贾谊于长沙,非无圣主;窜梁鸿于海曲,岂乏明时?<br>所赖君子见机,达人知命。<br>老当益壮,宁移白首之心?<br>穷且益坚,不坠青云之志。<br>酌贪泉而觉爽,处涸辙以犹欢。<br>北海虽赊,扶摇可接;东隅已逝,桑榆非晚。<br>孟尝高洁,空余报国之情;阮籍猖狂,岂效穷途之哭! |
|
|
||||||
| 自述 | 勃,三尺微命,一介书生。<br>无路请缨,等终军之弱冠;有怀投笔,慕宗悫之长风。<br>舍簪笏于百龄,奉晨昏于万里。<br>非谢家之宝树,接孟氏之芳邻。<br>他日趋庭,叨陪鲤对;今兹捧袂,喜托龙门。<br>杨意不逢,抚凌云而自惜;钟期既遇,奏流水以何惭? |
|
|
||||||
| 结尾 | 呜呼!胜地不常,盛筵难再;兰亭已矣,梓泽丘墟。<br>临别赠言,幸承恩于伟饯;登高作赋,是所望于群公。<br>敢竭鄙怀,恭疏短引;一言均赋,四韵俱成。<br>请洒潘江,各倾陆海云尔。 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 滕王阁诗
|
|
||||||
|
|
||||||
> 滕王高阁临江渚,佩玉鸣鸾罢歌舞。
|
|
||||||
> 画栋朝飞南浦云,珠帘暮卷西山雨。
|
|
||||||
> 闲云潭影日悠悠,物换星移几度秋。
|
|
||||||
> 阁中帝子今何在?槛外长江空自流。
|
|
||||||
`;
|
|
||||||
export const messageArr: BubbleListProps<MessageItem>['list'] = [
|
|
||||||
{
|
|
||||||
key: 1,
|
|
||||||
role: 'ai',
|
|
||||||
placement: 'start',
|
|
||||||
content: '欢迎使用 Element Plus X .'.repeat(5),
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'filled',
|
|
||||||
isMarkdown: false,
|
|
||||||
typing: { step: 2, suffix: '💗' },
|
|
||||||
avatar: avatar2,
|
|
||||||
avatarSize: '32px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 2,
|
|
||||||
role: 'user',
|
|
||||||
placement: 'end',
|
|
||||||
content: '这是用户的消息',
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'outlined',
|
|
||||||
isMarkdown: false,
|
|
||||||
avatar: avatar1,
|
|
||||||
avatarSize: '32px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 3,
|
|
||||||
role: 'ai',
|
|
||||||
placement: 'start',
|
|
||||||
content: '欢迎使用 Element Plus X .'.repeat(5),
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'filled',
|
|
||||||
isMarkdown: false,
|
|
||||||
typing: { step: 2, suffix: '💗' },
|
|
||||||
avatar: avatar2,
|
|
||||||
avatarSize: '32px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 4,
|
|
||||||
role: 'user',
|
|
||||||
placement: 'end',
|
|
||||||
content: '这是用户的消息',
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'outlined',
|
|
||||||
isMarkdown: false,
|
|
||||||
avatar: avatar1,
|
|
||||||
avatarSize: '32px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 5,
|
|
||||||
role: 'ai',
|
|
||||||
placement: 'start',
|
|
||||||
content: '欢迎使用 Element Plus X .'.repeat(5),
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'filled',
|
|
||||||
isMarkdown: false,
|
|
||||||
typing: { step: 2, suffix: '💗' },
|
|
||||||
avatar: avatar2,
|
|
||||||
avatarSize: '32px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 6,
|
|
||||||
role: 'user',
|
|
||||||
placement: 'end',
|
|
||||||
content: '这是用户的消息',
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'outlined',
|
|
||||||
isMarkdown: false,
|
|
||||||
avatar: avatar1,
|
|
||||||
avatarSize: '32px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 7,
|
|
||||||
role: 'ai',
|
|
||||||
placement: 'start',
|
|
||||||
content: '欢迎使用 Element Plus X .'.repeat(5),
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'filled',
|
|
||||||
isMarkdown: false,
|
|
||||||
typing: { step: 2, suffix: '💗', isRequestEnd: true },
|
|
||||||
avatar: avatar2,
|
|
||||||
avatarSize: '32px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 8,
|
|
||||||
role: 'user',
|
|
||||||
placement: 'end',
|
|
||||||
content: '这是用户的消息',
|
|
||||||
loading: true,
|
|
||||||
shape: 'corner',
|
|
||||||
variant: 'outlined',
|
|
||||||
isMarkdown: false,
|
|
||||||
avatar: avatar1,
|
|
||||||
avatarSize: '32px'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 模拟自定义文件卡片数据
|
|
||||||
// 内置样式
|
|
||||||
export const colorMap: Record<FilesType, string> = {
|
|
||||||
word: '#0078D4',
|
|
||||||
excel: '#00C851',
|
|
||||||
ppt: '#FF5722',
|
|
||||||
pdf: '#E53935',
|
|
||||||
txt: '#424242',
|
|
||||||
mark: '#6C6C6C',
|
|
||||||
image: '#FF80AB',
|
|
||||||
audio: '#FF7878',
|
|
||||||
video: '#8B72F7',
|
|
||||||
three: '#29B6F6',
|
|
||||||
code: '#00008B',
|
|
||||||
database: '#FF9800',
|
|
||||||
link: '#2962FF',
|
|
||||||
zip: '#673AB7',
|
|
||||||
file: '#FFC757',
|
|
||||||
unknown: '#6E9DA4'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自己定义文件颜色
|
|
||||||
export const colorMap1: Record<FilesType, string> = {
|
|
||||||
word: '#5E74A8',
|
|
||||||
excel: '#4A6B4A',
|
|
||||||
ppt: '#C27C40',
|
|
||||||
pdf: '#5A6976',
|
|
||||||
txt: '#D4C58C',
|
|
||||||
mark: '#FFA500',
|
|
||||||
image: '#8E7CC3',
|
|
||||||
audio: '#A67B5B',
|
|
||||||
video: '#4A5568',
|
|
||||||
three: '#5F9E86',
|
|
||||||
code: '#4B636E',
|
|
||||||
database: '#4A5A6B',
|
|
||||||
link: '#5D7CBA',
|
|
||||||
zip: '#8B5E3C',
|
|
||||||
file: '#AAB2BF',
|
|
||||||
unknown: '#888888'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自己定义文件颜色1
|
|
||||||
export const colorMap2: Record<FilesType, string> = {
|
|
||||||
word: '#0078D4',
|
|
||||||
excel: '#4CB050',
|
|
||||||
ppt: '#FF9933',
|
|
||||||
pdf: '#E81123',
|
|
||||||
txt: '#666666',
|
|
||||||
mark: '#FFA500',
|
|
||||||
image: '#B490F3',
|
|
||||||
audio: '#00B2EE',
|
|
||||||
video: '#2EC4B6',
|
|
||||||
three: '#00C8FF',
|
|
||||||
code: '#00589F',
|
|
||||||
database: '#F5A623',
|
|
||||||
link: '#007BFF',
|
|
||||||
zip: '#888888',
|
|
||||||
file: '#F0D9B5',
|
|
||||||
unknown: '#D8D8D8'
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Auto-Element-Plus-X by auto-export-all-components script
|
|
||||||
export { default as Attachments } from './components/Attachments/index.vue';
|
|
||||||
export { default as Bubble } from './components/Bubble/index.vue';
|
|
||||||
export { default as BubbleList } from './components/BubbleList/index.vue';
|
|
||||||
export { default as ConfigProvider } from './components/ConfigProvider/index.vue';
|
|
||||||
export { default as Conversations } from './components/Conversations/index.vue';
|
|
||||||
export { default as EditorSender } from './components/EditorSender/index.vue';
|
|
||||||
export { default as FilesCard } from './components/FilesCard/index.vue';
|
|
||||||
export { default as MentionSender } from './components/MentionSender/index.vue';
|
|
||||||
export { default as Prompts } from './components/Prompts/index.vue';
|
|
||||||
export { default as Sender } from './components/Sender/index.vue';
|
|
||||||
export { default as Thinking } from './components/Thinking/index.vue';
|
|
||||||
export { default as ThoughtChain } from './components/ThoughtChain/index.vue';
|
|
||||||
export { default as Typewriter } from './components/Typewriter/index.vue';
|
|
||||||
export { default as Welcome } from './components/Welcome/index.vue';
|
|
||||||
export { default as XMarkdown } from './components/XMarkdown/index.vue';
|
|
||||||
export { default as XMarkdownAsync } from './components/XMarkdownAsync/index.vue';
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { MarkdownProps } from '../XMarkdownCore/shared/types';
|
|
||||||
import { useShiki } from '@components/XMarkdownCore/hooks/useShiki';
|
|
||||||
import { MarkdownRenderer } from '../XMarkdownCore';
|
|
||||||
import { useMarkdownContext } from '../XMarkdownCore/components/MarkdownProvider';
|
|
||||||
import { DEFAULT_PROPS } from '../XMarkdownCore/shared/constants';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<MarkdownProps>(), DEFAULT_PROPS);
|
|
||||||
|
|
||||||
const slots = useSlots();
|
|
||||||
const customComponents = useMarkdownContext();
|
|
||||||
const colorReplacementsComputed = computed(() => {
|
|
||||||
return props.colorReplacements;
|
|
||||||
});
|
|
||||||
const needViewCodeBtnComputed = computed(() => {
|
|
||||||
return props.needViewCodeBtn;
|
|
||||||
});
|
|
||||||
useShiki();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="elx-xmarkdown-container">
|
|
||||||
<MarkdownRenderer
|
|
||||||
v-bind="props"
|
|
||||||
:color-replacements="colorReplacementsComputed"
|
|
||||||
:need-view-code-btn="needViewCodeBtnComputed"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="(slot, name) in customComponents"
|
|
||||||
:key="name"
|
|
||||||
#[name]="slotProps"
|
|
||||||
>
|
|
||||||
<component :is="slot" v-bind="slotProps" />
|
|
||||||
</template>
|
|
||||||
<template v-for="(_, name) in slots" :key="name" #[name]="slotProps">
|
|
||||||
<slot :name="name" v-bind="slotProps" />
|
|
||||||
</template>
|
|
||||||
</MarkdownRenderer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { defineComponent, h } from 'vue';
|
|
||||||
import {
|
|
||||||
MarkdownProvider,
|
|
||||||
useMarkdownContext
|
|
||||||
} from '../components/MarkdownProvider';
|
|
||||||
import { VueMarkdown, VueMarkdownAsync } from '../core';
|
|
||||||
import { useComponents } from '../hooks';
|
|
||||||
import { MARKDOWN_CORE_PROPS } from '../shared/constants';
|
|
||||||
|
|
||||||
const InnerRenderer = defineComponent({
|
|
||||||
name: 'InnerMarkdownRenderer',
|
|
||||||
setup(_, { slots }) {
|
|
||||||
const context = useMarkdownContext();
|
|
||||||
const components = useComponents();
|
|
||||||
return () =>
|
|
||||||
h(VueMarkdown, context.value as any, {
|
|
||||||
...components,
|
|
||||||
...slots
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const InnerRendererAsync = defineComponent({
|
|
||||||
name: 'InnerMarkdownRendererAsync',
|
|
||||||
setup(_, { slots }) {
|
|
||||||
const context: any = useMarkdownContext();
|
|
||||||
const components = useComponents();
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
h(VueMarkdownAsync, context.value, {
|
|
||||||
...components,
|
|
||||||
...slots
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const MarkdownRenderer = defineComponent({
|
|
||||||
name: 'MarkdownRenderer',
|
|
||||||
props: MARKDOWN_CORE_PROPS,
|
|
||||||
setup(props, { slots }) {
|
|
||||||
return () =>
|
|
||||||
h(MarkdownProvider, props, {
|
|
||||||
default: () => h(InnerRenderer, {}, slots)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const MarkdownRendererAsync = defineComponent({
|
|
||||||
name: 'MarkdownRendererAsync',
|
|
||||||
props: MARKDOWN_CORE_PROPS,
|
|
||||||
setup(props, { slots }) {
|
|
||||||
return () =>
|
|
||||||
h(MarkdownProvider, props, {
|
|
||||||
default: () => h(InnerRendererAsync, {}, slots)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export { MarkdownRenderer, MarkdownRendererAsync };
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# 使用
|
|
||||||
|
|
||||||
```vue
|
|
||||||
import {MarkdownRenderer,MarkdownRendererAsync} from '@/components/Markdown';
|
|
||||||
|
|
||||||
<template>
|
|
||||||
// 同步渲染
|
|
||||||
<MarkdownRenderer class="markdown-render" :markdown="content" />
|
|
||||||
|
|
||||||
// 异步渲染
|
|
||||||
<Suspense>
|
|
||||||
<MarkdownRendererAsync class="markdown-render" :markdown="content" />
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 属性
|
|
||||||
|
|
||||||
### customAttrs 自定义属性支持
|
|
||||||
|
|
||||||
通过 `customAttrs` 可以对 Markdown 渲染的节点动态添加自定义属性:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const customAttrs = {
|
|
||||||
heading: (node, { level }) => ({
|
|
||||||
class: ['heading', `heading-${level}`]
|
|
||||||
}),
|
|
||||||
a: node => ({
|
|
||||||
target: '_blank',
|
|
||||||
rel: 'noopener noreferrer'
|
|
||||||
})
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 插槽
|
|
||||||
|
|
||||||
> 组件提供了多个插槽,可以自定义渲染,标签即为插槽,你可以接管任何插槽,自定义渲染逻辑。
|
|
||||||
|
|
||||||
**请注意:组件内部拦截了code标签的渲染,支持高亮代码块,mermaid图表等。如果你需要自定义渲染,可以接管code插槽。**
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<header></header>
|
|
||||||
|
|
||||||
<MarkdownRenderer>
|
|
||||||
<template #heading="{ node, level }">
|
|
||||||
可自定义标题渲染
|
|
||||||
</template>
|
|
||||||
</MarkdownRenderer>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 代码块渲染
|
|
||||||
|
|
||||||
组件内置了代码块渲染器,支持高亮代码块,mermaid图表等。
|
|
||||||
codeXSlot自定义代码块顶部
|
|
||||||
可通过 codeXRender 属性自定义代码块语言渲染器,如下可以自定义 echarts 渲染器:
|
|
||||||
|
|
||||||
```text
|
|
||||||
codeXRender: {
|
|
||||||
echarts: (props) => {
|
|
||||||
return h()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { CopyDocument, Select } from '@element-plus/icons-vue';
|
|
||||||
import { ElButton } from 'element-plus';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
onCopy: () => void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const copied = ref(false);
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
if (!copied.value) {
|
|
||||||
props.onCopy();
|
|
||||||
copied.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
copied.value = false;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElButton
|
|
||||||
class="shiki-header-button markdown-elxLanguage-header-button"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="copied ? Select : CopyDocument"
|
|
||||||
class="markdown-elxLanguage-header-button-text"
|
|
||||||
:class="[copied && 'copied']"
|
|
||||||
/>
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { GlobalShiki } from '@components/XMarkdownCore/hooks/useShiki';
|
|
||||||
import type { BundledLanguage } from 'shiki';
|
|
||||||
import type { ElxRunCodeProps } from '../RunCode/type';
|
|
||||||
import type { CodeBlockExpose } from './shiki-header';
|
|
||||||
import type { RawProps } from './types';
|
|
||||||
import {
|
|
||||||
transformerNotationDiff,
|
|
||||||
transformerNotationErrorLevel,
|
|
||||||
transformerNotationFocus,
|
|
||||||
transformerNotationHighlight,
|
|
||||||
transformerNotationWordHighlight
|
|
||||||
} from '@shikijs/transformers';
|
|
||||||
import { computed, h, reactive, ref, toValue, watch, onUnmounted } from 'vue';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import HighLightCode from '../../components/HighLightCode/index.vue';
|
|
||||||
import { SHIKI_SUPPORT_LANGS, shikiThemeDefault } from '../../shared';
|
|
||||||
import { useMarkdownContext } from '../MarkdownProvider';
|
|
||||||
import RunCode from '../RunCode/index.vue';
|
|
||||||
import {
|
|
||||||
controlEle,
|
|
||||||
controlHasRunCodeEle,
|
|
||||||
copyCode,
|
|
||||||
isDark,
|
|
||||||
languageEle,
|
|
||||||
toggleExpand,
|
|
||||||
toggleTheme
|
|
||||||
} from './shiki-header';
|
|
||||||
import '../../style/shiki.scss';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
raw?: RawProps;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
raw: () => ({})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const context = useMarkdownContext();
|
|
||||||
const { codeXSlot, customAttrs, globalShiki } = toValue(context) || {};
|
|
||||||
const renderLines = ref<string[]>([]);
|
|
||||||
const preStyle = ref<any | null>(null);
|
|
||||||
const preClass = ref<string | null>(null);
|
|
||||||
const themes = computed(() => context?.value?.themes ?? shikiThemeDefault);
|
|
||||||
const colorReplacements = computed(() => context?.value?.colorReplacements);
|
|
||||||
const nowViewBtnShow = computed(() => context?.value?.needViewCodeBtn ?? false);
|
|
||||||
const viewCodeModalOptions = computed(
|
|
||||||
() => context?.value?.viewCodeModalOptions
|
|
||||||
);
|
|
||||||
const isExpand = ref(true);
|
|
||||||
const nowCodeLanguage = ref<BundledLanguage>();
|
|
||||||
const codeAttrs =
|
|
||||||
typeof customAttrs?.code === 'function'
|
|
||||||
? customAttrs.code(props.raw)
|
|
||||||
: customAttrs?.code || {};
|
|
||||||
const shikiTransformers = [
|
|
||||||
transformerNotationDiff(),
|
|
||||||
transformerNotationErrorLevel(),
|
|
||||||
transformerNotationFocus(),
|
|
||||||
transformerNotationHighlight(),
|
|
||||||
transformerNotationWordHighlight()
|
|
||||||
];
|
|
||||||
|
|
||||||
const { codeToHtml } = globalShiki as GlobalShiki;
|
|
||||||
// 生成高亮HTML
|
|
||||||
async function generateHtml() {
|
|
||||||
let { language = 'text', content = '' } = props.raw || {};
|
|
||||||
if (!(SHIKI_SUPPORT_LANGS as readonly string[]).includes(language)) {
|
|
||||||
language = 'text';
|
|
||||||
}
|
|
||||||
nowCodeLanguage.value = language as BundledLanguage;
|
|
||||||
const html = await codeToHtml(content.trim(), {
|
|
||||||
lang: language as BundledLanguage,
|
|
||||||
themes: themes.value,
|
|
||||||
colorReplacements: colorReplacements.value,
|
|
||||||
transformers: shikiTransformers
|
|
||||||
});
|
|
||||||
const parse = new DOMParser();
|
|
||||||
const doc = parse.parseFromString(html, 'text/html');
|
|
||||||
const preElement = doc.querySelector('pre');
|
|
||||||
preStyle.value = preElement?.getAttribute('style');
|
|
||||||
const preClassNames = preElement?.className;
|
|
||||||
preClass.value = preClassNames ?? '';
|
|
||||||
const codeElement = doc.querySelector('pre code');
|
|
||||||
if (codeElement) {
|
|
||||||
const lines = codeElement.querySelectorAll('.line'); // 获取所有代码行
|
|
||||||
renderLines.value = Array.from(lines).map(line => line.outerHTML); // 存储每行HTML
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用防抖优化代码块渲染,避免SSE流式更新时频繁高亮
|
|
||||||
const debouncedGenerateHtml = debounce(generateHtml, 100, {
|
|
||||||
leading: false,
|
|
||||||
trailing: true,
|
|
||||||
maxWait: 200
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.raw?.content,
|
|
||||||
async content => {
|
|
||||||
if (content) {
|
|
||||||
debouncedGenerateHtml();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清理防抖定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
debouncedGenerateHtml.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const runCodeOptions = reactive<ElxRunCodeProps>({
|
|
||||||
code: [],
|
|
||||||
content: '',
|
|
||||||
visible: false,
|
|
||||||
lang: '',
|
|
||||||
preClass: '',
|
|
||||||
preStyle: {}
|
|
||||||
});
|
|
||||||
function viewCode(renderLines: string[]) {
|
|
||||||
if (!renderLines?.length) return;
|
|
||||||
|
|
||||||
Object.assign(runCodeOptions, {
|
|
||||||
code: renderLines,
|
|
||||||
content: props.raw?.content ?? '',
|
|
||||||
lang: nowCodeLanguage.value || 'html',
|
|
||||||
preClass: preClass.value || 'pre-md',
|
|
||||||
preStyle: preStyle.value || {},
|
|
||||||
visible: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => renderLines.value,
|
|
||||||
val => {
|
|
||||||
if (runCodeOptions.visible) {
|
|
||||||
runCodeOptions.code = val;
|
|
||||||
runCodeOptions.content = props.raw.content ?? '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染插槽函数
|
|
||||||
function renderSlot(slotName: string) {
|
|
||||||
if (!codeXSlot) {
|
|
||||||
return 'div';
|
|
||||||
}
|
|
||||||
const slotFn = codeXSlot[slotName];
|
|
||||||
if (typeof slotFn === 'function') {
|
|
||||||
return slotFn({
|
|
||||||
...props,
|
|
||||||
renderLines: renderLines.value,
|
|
||||||
isDark,
|
|
||||||
isExpand,
|
|
||||||
nowViewBtnShow: nowViewBtnShow.value,
|
|
||||||
toggleExpand,
|
|
||||||
toggleTheme,
|
|
||||||
copyCode,
|
|
||||||
viewCode
|
|
||||||
} satisfies CodeBlockExpose);
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(slotFn as any, {
|
|
||||||
...props,
|
|
||||||
renderLines: renderLines.value,
|
|
||||||
isDark,
|
|
||||||
isExpand,
|
|
||||||
nowViewBtnShow: nowViewBtnShow.value,
|
|
||||||
toggleExpand,
|
|
||||||
toggleTheme,
|
|
||||||
copyCode,
|
|
||||||
viewCode
|
|
||||||
} satisfies CodeBlockExpose);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleHeaderLanguageClick() {
|
|
||||||
isExpand.value = !isExpand.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
const computedClass = computed(() => `pre-md ${preClass.value} is-expanded`);
|
|
||||||
const codeClass = computed(() => `language-${props.raw?.language || 'text'}`);
|
|
||||||
const RunCodeComputed = computed(() => {
|
|
||||||
return nowCodeLanguage.value === 'html' && nowViewBtnShow.value
|
|
||||||
? RunCode
|
|
||||||
: undefined;
|
|
||||||
});
|
|
||||||
const codeControllerEleComputed = computed(() => {
|
|
||||||
if (nowCodeLanguage.value === 'html' && nowViewBtnShow.value) {
|
|
||||||
return controlHasRunCodeEle(
|
|
||||||
() => {
|
|
||||||
copyCode(renderLines.value);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
viewCode(renderLines.value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return controlEle(() => {
|
|
||||||
copyCode(renderLines.value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => nowViewBtnShow.value,
|
|
||||||
v => {
|
|
||||||
if (!v) {
|
|
||||||
runCodeOptions.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取是否显示行号
|
|
||||||
const enableCodeLineNumber = computed(() => {
|
|
||||||
return context?.value?.codeXProps?.enableCodeLineNumber ?? false;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :key="props.raw?.key" :class="computedClass" :style="preStyle">
|
|
||||||
<div class="markdown-elxLanguage-header-div is-always-shadow">
|
|
||||||
<component
|
|
||||||
:is="renderSlot('codeHeader')"
|
|
||||||
v-if="codeXSlot?.codeHeader && renderSlot('codeHeader')"
|
|
||||||
/>
|
|
||||||
<template v-else>
|
|
||||||
<component
|
|
||||||
:is="
|
|
||||||
codeXSlot?.codeHeaderLanguage
|
|
||||||
? renderSlot('codeHeaderLanguage')
|
|
||||||
: languageEle(props.raw?.language ?? 'text')
|
|
||||||
"
|
|
||||||
@click="handleHeaderLanguageClick"
|
|
||||||
/>
|
|
||||||
<component
|
|
||||||
:is="
|
|
||||||
codeXSlot?.codeHeaderControl
|
|
||||||
? renderSlot('codeHeaderControl')
|
|
||||||
: codeControllerEleComputed
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<code
|
|
||||||
:class="codeClass"
|
|
||||||
:style="{
|
|
||||||
display: 'block',
|
|
||||||
overflowX: 'auto'
|
|
||||||
}"
|
|
||||||
v-bind="codeAttrs"
|
|
||||||
>
|
|
||||||
<HighLightCode
|
|
||||||
:enable-code-line-number="enableCodeLineNumber"
|
|
||||||
:lang="props.raw?.language ?? 'text'"
|
|
||||||
:code="renderLines"
|
|
||||||
/>
|
|
||||||
</code>
|
|
||||||
<!-- run-code -->
|
|
||||||
<component
|
|
||||||
:is="RunCodeComputed"
|
|
||||||
v-bind="{ ...viewCodeModalOptions, ...runCodeOptions }"
|
|
||||||
v-model:visible="runCodeOptions.visible"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { View } from '@element-plus/icons-vue';
|
|
||||||
import { ElButton } from 'element-plus';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
onView: () => void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
props.onView();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElButton
|
|
||||||
class="shiki-header-button markdown-elxLanguage-header-button"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<View />
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
@@ -1,491 +0,0 @@
|
|||||||
import type { Component, Ref, VNode } from 'vue';
|
|
||||||
import type { MermaidExposeProps } from '../Mermaid/types';
|
|
||||||
import type {
|
|
||||||
ElxRunCodeCloseBtnExposeProps,
|
|
||||||
ElxRunCodeContentExposeProps,
|
|
||||||
ElxRunCodeExposeProps
|
|
||||||
} from '../RunCode/type';
|
|
||||||
import type { RawProps } from './types';
|
|
||||||
import { useMarkdownContext } from '@components/XMarkdownCore/components/MarkdownProvider';
|
|
||||||
import { ArrowDownBold, Moon, Sunny } from '@element-plus/icons-vue';
|
|
||||||
import { ElButton, ElMessage, ElSpace } from 'element-plus';
|
|
||||||
import { h, ref } from 'vue';
|
|
||||||
import CopyCodeButton from './copy-code-button.vue';
|
|
||||||
import RunCodeButton from './run-code-button.vue';
|
|
||||||
|
|
||||||
export interface CodeBlockExpose {
|
|
||||||
/**
|
|
||||||
* 代码块传入的代码原始数据属性
|
|
||||||
*/
|
|
||||||
raw: RawProps;
|
|
||||||
/**
|
|
||||||
* 渲染的行
|
|
||||||
*/
|
|
||||||
renderLines: Array<string>;
|
|
||||||
/**
|
|
||||||
* 当前主题色是否是暗色
|
|
||||||
*/
|
|
||||||
isDark: Ref<boolean>;
|
|
||||||
/**
|
|
||||||
* 当前代码块是否展开
|
|
||||||
*/
|
|
||||||
isExpand: Ref<boolean>;
|
|
||||||
/**
|
|
||||||
* 是否显示预览代码按钮
|
|
||||||
*/
|
|
||||||
nowViewBtnShow: boolean;
|
|
||||||
/**
|
|
||||||
* 切换展开折叠
|
|
||||||
* @param ev MouseEvent
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
toggleExpand: (ev: MouseEvent) => { isExpand: boolean };
|
|
||||||
/**
|
|
||||||
* 切换主题
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
toggleTheme: () => boolean;
|
|
||||||
/**
|
|
||||||
* 复制代码
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
copyCode: (value: string | Array<string>) => void;
|
|
||||||
/**
|
|
||||||
* 查看代码
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
viewCode: (value: Array<string>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ComponentRenderer<T> = Component<T>;
|
|
||||||
|
|
||||||
export type ComponentFunctionRenderer<T> = (props: T) => VNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 代码块头部渲染器
|
|
||||||
*/
|
|
||||||
export type CodeBlockHeaderRenderer = ComponentRenderer<CodeBlockExpose>;
|
|
||||||
export type CodeBlockHeaderFunctionRenderer =
|
|
||||||
ComponentFunctionRenderer<CodeBlockExpose>;
|
|
||||||
/**
|
|
||||||
* @description 查看代码头部渲染器
|
|
||||||
*/
|
|
||||||
export type ViewCodeHeadRender = ComponentRenderer<ElxRunCodeExposeProps>;
|
|
||||||
export type ViewCodeHeadFunctionRender =
|
|
||||||
ComponentFunctionRenderer<ElxRunCodeExposeProps>;
|
|
||||||
/**
|
|
||||||
* @description 查看代码头部关闭按钮渲染器
|
|
||||||
*/
|
|
||||||
export type ViewCodeCloseBtnRender =
|
|
||||||
ComponentRenderer<ElxRunCodeCloseBtnExposeProps>;
|
|
||||||
export type ViewCodeCloseBtnFunctionRender =
|
|
||||||
ComponentFunctionRenderer<ElxRunCodeCloseBtnExposeProps>;
|
|
||||||
/**
|
|
||||||
* @description 查看代码内容渲染器
|
|
||||||
*/
|
|
||||||
export type ViewCodeContentRender =
|
|
||||||
ComponentRenderer<ElxRunCodeContentExposeProps>;
|
|
||||||
export type ViewCodeContentFunctionRender =
|
|
||||||
ComponentFunctionRenderer<ElxRunCodeContentExposeProps>;
|
|
||||||
/**
|
|
||||||
* @description Mermaid头部插槽渲染器
|
|
||||||
*/
|
|
||||||
export type MermaidHeaderControlRender = ComponentRenderer<MermaidExposeProps>;
|
|
||||||
export type MermaidHeaderControlFunctionRender =
|
|
||||||
ComponentFunctionRenderer<MermaidExposeProps>;
|
|
||||||
|
|
||||||
export interface CodeBlockHeaderExpose {
|
|
||||||
/**
|
|
||||||
* 代码块自定义头部(包括语言和复制按钮等)
|
|
||||||
* 当有此属性时,将不会显示默认的代码头部 和 codeHeaderLanguage codeHeaderControl 插槽里面的内容
|
|
||||||
*/
|
|
||||||
codeHeader?: CodeBlockHeaderRenderer;
|
|
||||||
/**
|
|
||||||
* 代码块语言插槽
|
|
||||||
*/
|
|
||||||
codeHeaderLanguage?: CodeBlockHeaderRenderer;
|
|
||||||
/**
|
|
||||||
* 代码块右侧插槽
|
|
||||||
*/
|
|
||||||
codeHeaderControl?: CodeBlockHeaderRenderer;
|
|
||||||
/**
|
|
||||||
* 代码块查看代码弹窗的头部插槽
|
|
||||||
*/
|
|
||||||
viewCodeHeader?: ViewCodeHeadRender;
|
|
||||||
/**
|
|
||||||
* 代码块查看代码弹窗的关闭按钮插槽
|
|
||||||
*/
|
|
||||||
viewCodeCloseBtn?: ViewCodeCloseBtnRender;
|
|
||||||
/**
|
|
||||||
* 代码块查看代码弹窗的代码内容插槽
|
|
||||||
*/
|
|
||||||
viewCodeContent?: ViewCodeContentRender;
|
|
||||||
/**
|
|
||||||
* 代码块mermaid头部插槽
|
|
||||||
*/
|
|
||||||
codeMermaidHeaderControl?: MermaidHeaderControlRender;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CodeBlockHeaderFunctionExpose {
|
|
||||||
/**
|
|
||||||
* 代码块自定义头部(包括语言和复制按钮等)
|
|
||||||
* 当有此属性时,将不会显示默认的代码头部 和 codeHeaderLanguage codeHeaderControl 插槽里面的内容
|
|
||||||
*/
|
|
||||||
codeHeader?: CodeBlockHeaderFunctionRenderer;
|
|
||||||
/**
|
|
||||||
* 代码块语言插槽
|
|
||||||
*/
|
|
||||||
codeHeaderLanguage?: CodeBlockHeaderFunctionRenderer;
|
|
||||||
/**
|
|
||||||
* 代码块右侧插槽
|
|
||||||
*/
|
|
||||||
codeHeaderControl?: CodeBlockHeaderFunctionRenderer;
|
|
||||||
/**
|
|
||||||
* 代码块查看代码弹窗的头部插槽
|
|
||||||
*/
|
|
||||||
viewCodeHeader?: ViewCodeHeadFunctionRender;
|
|
||||||
/**
|
|
||||||
* 代码块查看代码弹窗的关闭按钮插槽
|
|
||||||
*/
|
|
||||||
viewCodeCloseBtn?: ViewCodeCloseBtnFunctionRender;
|
|
||||||
/**
|
|
||||||
* 代码块查看代码弹窗的代码内容插槽
|
|
||||||
*/
|
|
||||||
viewCodeContent?: ViewCodeContentFunctionRender;
|
|
||||||
/**
|
|
||||||
* 代码块mermaid头部插槽
|
|
||||||
*/
|
|
||||||
codeMermaidHeaderControl?: MermaidHeaderControlFunctionRender;
|
|
||||||
}
|
|
||||||
|
|
||||||
let copyCodeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
// 记录当前是否暗色模式
|
|
||||||
export const isDark = ref(document.body.classList.contains('dark'));
|
|
||||||
|
|
||||||
/* ----------------------------------- 按钮组 ---------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 language标签
|
|
||||||
* @date 2025-06-25 17:48:15
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param language
|
|
||||||
*/
|
|
||||||
export function languageEle(language: string) {
|
|
||||||
return h(
|
|
||||||
ElSpace,
|
|
||||||
{
|
|
||||||
class: `markdown-elxLanguage-header-space markdown-elxLanguage-header-space-start markdown-elxLanguage-header-span`,
|
|
||||||
direction: 'horizontal',
|
|
||||||
onClick: (ev: MouseEvent) => {
|
|
||||||
toggleExpand(ev);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => [
|
|
||||||
h(
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: 'markdown-elxLanguage-header-span is-always-shadow'
|
|
||||||
},
|
|
||||||
language || ''
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
ElButton,
|
|
||||||
{
|
|
||||||
class: 'shiki-header-button shiki-header-button-expand'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => [
|
|
||||||
h(ArrowDownBold, {
|
|
||||||
class:
|
|
||||||
'markdown-elxLanguage-header-toggle markdown-elxLanguage-header-toggle-expand '
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 语言头部操作按钮
|
|
||||||
* @date 2025-06-25 17:49:04
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param {() => void} copy
|
|
||||||
*/
|
|
||||||
export function controlEle(copy: () => void) {
|
|
||||||
return h(
|
|
||||||
ElSpace,
|
|
||||||
{
|
|
||||||
class: `markdown-elxLanguage-header-space`,
|
|
||||||
direction: 'horizontal'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => [
|
|
||||||
toggleThemeEle(),
|
|
||||||
h(CopyCodeButton, { onCopy: copy }) // ✅ 改为组件形式
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 语言头部操作按钮(带预览代码按钮)
|
|
||||||
* @date 2025-07-09 11:15:27
|
|
||||||
* @author tingfeng
|
|
||||||
* @param copy
|
|
||||||
* @param view
|
|
||||||
*/
|
|
||||||
export function controlHasRunCodeEle(copy: () => void, view: () => void) {
|
|
||||||
const context = useMarkdownContext();
|
|
||||||
const { codeXProps } = toValue(context) || {};
|
|
||||||
return h(
|
|
||||||
ElSpace,
|
|
||||||
{
|
|
||||||
class: `markdown-elxLanguage-header-space`,
|
|
||||||
direction: 'horizontal'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => [
|
|
||||||
codeXProps?.enableCodePreview
|
|
||||||
? h(RunCodeButton, { onView: view })
|
|
||||||
: null,
|
|
||||||
codeXProps?.enableThemeToggle ? toggleThemeEle() : null,
|
|
||||||
codeXProps?.enableCodeCopy ? h(CopyCodeButton, { onCopy: copy }) : null // ✅ 改为组件形式
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 主题按钮
|
|
||||||
* @date 2025-06-25 17:49:53
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export function toggleThemeEle() {
|
|
||||||
return h(
|
|
||||||
ElButton,
|
|
||||||
{
|
|
||||||
class: 'shiki-header-button markdown-elxLanguage-header-toggle',
|
|
||||||
onClick: () => {
|
|
||||||
toggleTheme();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () =>
|
|
||||||
h(!isDark.value ? Moon : Sunny, {
|
|
||||||
class: 'markdown-elxLanguage-header-toggle'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------- 方法 ----------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 展开代码
|
|
||||||
* @date 2025-07-01 11:33:32
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param elem
|
|
||||||
*/
|
|
||||||
export function expand(elem: HTMLElement) {
|
|
||||||
elem.classList.add('is-expanded');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 折叠代码
|
|
||||||
* @date 2025-07-01 11:33:49
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param elem
|
|
||||||
*/
|
|
||||||
export function collapse(elem: HTMLElement) {
|
|
||||||
elem.classList.remove('is-expanded');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 复制代码内容到剪贴板
|
|
||||||
* @date 2025-03-28 14:03:22
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param v
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
async function copy(v: string) {
|
|
||||||
try {
|
|
||||||
// 现代浏览器 Clipboard API
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
await navigator.clipboard.writeText(v);
|
|
||||||
ElMessage({
|
|
||||||
message: '复制成功',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
return; // 复制成功直接返回
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容旧浏览器的 execCommand 方案
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = v.trim();
|
|
||||||
textarea.style.position = 'fixed'; // 避免滚动到文本框位置
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
|
|
||||||
// 执行复制命令
|
|
||||||
const success = document.execCommand('copy');
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
if (success) {
|
|
||||||
ElMessage({
|
|
||||||
message: '复制成功',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
return; // 复制成功直接返回
|
|
||||||
}
|
|
||||||
if (!success) {
|
|
||||||
throw new Error('复制失败,请检查浏览器权限');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`复制失败: ${err instanceof Error ? err.message : String(err)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 将源代码行数转换可复制的string
|
|
||||||
* @date 2025-06-25 17:50:42
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param lines
|
|
||||||
*/
|
|
||||||
export function extractCodeFromHtmlLines(lines: string[]): string {
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const output: string[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
container.innerHTML = lines[i];
|
|
||||||
const text = container.textContent?.trimEnd();
|
|
||||||
output.push(text ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
container.remove();
|
|
||||||
container.innerHTML = ''; // 清空引用内容
|
|
||||||
container.textContent = null;
|
|
||||||
|
|
||||||
return output.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
let isToggling = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 切换展开状态
|
|
||||||
* @date 2025-06-26 21:29:50
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param ev
|
|
||||||
*/
|
|
||||||
export function toggleExpand(ev: MouseEvent): { isExpand: boolean } {
|
|
||||||
if (isToggling) return { isExpand: false }; // 防抖保护
|
|
||||||
isToggling = true;
|
|
||||||
|
|
||||||
const ele = ev.currentTarget as HTMLElement;
|
|
||||||
const preMd = ele.closest('.pre-md') as HTMLElement | null;
|
|
||||||
|
|
||||||
if (preMd) {
|
|
||||||
setTimeout(() => {
|
|
||||||
isToggling = false;
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
if (preMd.classList.contains('is-expanded')) {
|
|
||||||
collapse(preMd);
|
|
||||||
return { isExpand: false };
|
|
||||||
} else {
|
|
||||||
expand(preMd);
|
|
||||||
return { isExpand: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isToggling = false;
|
|
||||||
return { isExpand: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 切换主题
|
|
||||||
* @date 2025-06-26 21:58:56
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
*/
|
|
||||||
export function toggleTheme() {
|
|
||||||
const theme = document.body.classList.contains('dark') ? 'light' : 'dark';
|
|
||||||
isDark.value = theme === 'dark';
|
|
||||||
if (isDark.value) {
|
|
||||||
document.body.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('dark');
|
|
||||||
}
|
|
||||||
return isDark.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 初始化主题模式
|
|
||||||
* @date 2025-07-08 13:43:19
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param defaultThemeMode
|
|
||||||
*/
|
|
||||||
export function initThemeMode(defaultThemeMode: 'light' | 'dark') {
|
|
||||||
const theme = document.body.classList.contains('dark') ? 'dark' : 'light';
|
|
||||||
if (theme !== defaultThemeMode) {
|
|
||||||
isDark.value = defaultThemeMode === 'dark';
|
|
||||||
if (defaultThemeMode === 'dark') {
|
|
||||||
document.body.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 描述 复制代码
|
|
||||||
* @date 2025-06-26 22:02:57
|
|
||||||
* @author tingfeng
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param codeText
|
|
||||||
*/
|
|
||||||
export function copyCode(codeText: string | string[]) {
|
|
||||||
try {
|
|
||||||
if (copyCodeTimer) return false; // 阻止重复点击
|
|
||||||
|
|
||||||
if (Array.isArray(codeText)) {
|
|
||||||
const code = extractCodeFromHtmlLines(codeText);
|
|
||||||
copy(code);
|
|
||||||
} else {
|
|
||||||
copy(codeText);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyCodeTimer = setTimeout(() => {
|
|
||||||
copyCodeTimer = null;
|
|
||||||
}, 800);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('🚀 ~ copyCode ~ error:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface RawProps {
|
|
||||||
language?: string;
|
|
||||||
content?: string;
|
|
||||||
key?: string | number;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { CodeLineProps } from './types';
|
|
||||||
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<CodeLineProps>(), {
|
|
||||||
raw: () => ({}),
|
|
||||||
content: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取实际内容
|
|
||||||
const content = computed(() => {
|
|
||||||
const result = props.raw?.content || props.content || '';
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span class="inline-code-tag">
|
|
||||||
{{ content }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.inline-code-tag {
|
|
||||||
display: inline;
|
|
||||||
background: #d7e2f8;
|
|
||||||
color: #376fde;
|
|
||||||
padding: 0 4px;
|
|
||||||
margin: 0 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 500;
|
|
||||||
border: 1px solid #d7e2f8;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-all;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface CodeLineProps {
|
|
||||||
raw?: {
|
|
||||||
content?: string;
|
|
||||||
inline?: boolean;
|
|
||||||
};
|
|
||||||
content?: string;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { defineComponent, h, toValue } from 'vue';
|
|
||||||
import { CodeBlock, CodeLine, Mermaid } from '../index';
|
|
||||||
import { useMarkdownContext } from '../MarkdownProvider';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
raw: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const context = useMarkdownContext();
|
|
||||||
const { codeXRender } = toValue(context);
|
|
||||||
return (): ReturnType<typeof h> | null => {
|
|
||||||
if (props.raw.inline) {
|
|
||||||
if (codeXRender && codeXRender.inline) {
|
|
||||||
const renderer = codeXRender.inline;
|
|
||||||
if (typeof renderer === 'function') {
|
|
||||||
return renderer(props);
|
|
||||||
}
|
|
||||||
return h(renderer, props);
|
|
||||||
}
|
|
||||||
return h(CodeLine, { raw: props.raw });
|
|
||||||
}
|
|
||||||
const { language } = props.raw;
|
|
||||||
if (codeXRender && codeXRender[language]) {
|
|
||||||
const renderer = codeXRender[language];
|
|
||||||
if (typeof renderer === 'function') {
|
|
||||||
return renderer(props);
|
|
||||||
}
|
|
||||||
return h(renderer, props);
|
|
||||||
}
|
|
||||||
if (language === 'mermaid') {
|
|
||||||
return h(Mermaid, props);
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(CodeBlock, props);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { ElScrollbar } from 'element-plus';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
export interface HighLightCodeProps {
|
|
||||||
code: string[];
|
|
||||||
lang: string;
|
|
||||||
enableCodeLineNumber: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<HighLightCodeProps>();
|
|
||||||
|
|
||||||
const codeClass = computed(() => `language-${props.lang || 'text'}`);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="elx-highlight-code-wrapper">
|
|
||||||
<div v-if="props.enableCodeLineNumber" class="line-numbers">
|
|
||||||
<span
|
|
||||||
v-for="(_line, index) in props.code"
|
|
||||||
:key="index"
|
|
||||||
class="line-number"
|
|
||||||
>
|
|
||||||
{{ index + 1 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ElScrollbar class="elx-highlight-code-scrollbar">
|
|
||||||
<div class="code-lines" :class="codeClass">
|
|
||||||
<span
|
|
||||||
v-for="(line, index) in props.code"
|
|
||||||
:key="index"
|
|
||||||
class="line-content"
|
|
||||||
v-html="line"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElScrollbar>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss"></style>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
.elx-highlight-code-wrapper {
|
|
||||||
display: flex;
|
|
||||||
background: transparent;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.line-numbers {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
margin-right: 1rem;
|
|
||||||
.line-number {
|
|
||||||
display: inline-block;
|
|
||||||
text-align: right;
|
|
||||||
padding: 0 0 0 0.3em;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.elx-highlight-code-scrollbar {
|
|
||||||
.code-lines {
|
|
||||||
white-space: pre;
|
|
||||||
& > span {
|
|
||||||
width: max-content;
|
|
||||||
display: block;
|
|
||||||
.line {
|
|
||||||
width: max-content;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: pre;
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import type { GlobalShiki } from '@components/XMarkdownCore/hooks/useShiki';
|
|
||||||
import type { Ref } from 'vue';
|
|
||||||
|
|
||||||
import type { MarkdownContext } from './types';
|
|
||||||
import deepmerge from 'deepmerge';
|
|
||||||
|
|
||||||
import { computed, defineComponent, h, inject, provide } from 'vue';
|
|
||||||
import {
|
|
||||||
useDarkModeWatcher,
|
|
||||||
usePlugins,
|
|
||||||
useProcessMarkdown
|
|
||||||
} from '../../hooks';
|
|
||||||
import { GLOBAL_SHIKI_KEY, MARKDOWN_PROVIDER_KEY } from '../../shared';
|
|
||||||
import { MARKDOWN_CORE_PROPS } from '../../shared/constants';
|
|
||||||
import { initThemeMode } from '../CodeBlock/shiki-header';
|
|
||||||
import '../../style/index.scss';
|
|
||||||
|
|
||||||
const MarkdownProvider = defineComponent({
|
|
||||||
name: 'MarkdownProvider',
|
|
||||||
props: MARKDOWN_CORE_PROPS,
|
|
||||||
setup(props, { slots, attrs }) {
|
|
||||||
const { rehypePlugins, remarkPlugins } = usePlugins(props);
|
|
||||||
const { isDark } = useDarkModeWatcher();
|
|
||||||
const globalShiki = inject<GlobalShiki>(GLOBAL_SHIKI_KEY);
|
|
||||||
const markdown = computed(() => {
|
|
||||||
if (props.enableLatex) {
|
|
||||||
return useProcessMarkdown(props.markdown);
|
|
||||||
} else {
|
|
||||||
return props.markdown;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const processProps = computed(() => {
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
codeXProps: Object.assign(
|
|
||||||
{},
|
|
||||||
MARKDOWN_CORE_PROPS.codeXProps.default(),
|
|
||||||
props.codeXProps
|
|
||||||
),
|
|
||||||
markdown: markdown.value
|
|
||||||
};
|
|
||||||
});
|
|
||||||
watch(
|
|
||||||
() => props.defaultThemeMode,
|
|
||||||
v => {
|
|
||||||
if (v) {
|
|
||||||
initThemeMode(v);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const contextProps = computed(() => {
|
|
||||||
return deepmerge(
|
|
||||||
{
|
|
||||||
rehypePlugins: toValue(rehypePlugins),
|
|
||||||
remarkPlugins: toValue(remarkPlugins),
|
|
||||||
isDark: toValue(isDark),
|
|
||||||
globalShiki: toValue(globalShiki)
|
|
||||||
},
|
|
||||||
processProps.value
|
|
||||||
);
|
|
||||||
});
|
|
||||||
provide(MARKDOWN_PROVIDER_KEY, contextProps);
|
|
||||||
return () =>
|
|
||||||
h(
|
|
||||||
'div',
|
|
||||||
{ class: 'elx-xmarkdown-provider', ...attrs },
|
|
||||||
slots.default && slots.default()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function useMarkdownContext(): Ref<MarkdownContext> {
|
|
||||||
const context = inject<Ref<MarkdownContext>>(
|
|
||||||
MARKDOWN_PROVIDER_KEY,
|
|
||||||
computed(() => ({}))
|
|
||||||
);
|
|
||||||
if (!context) {
|
|
||||||
return computed(() => ({})) as unknown as Ref<MarkdownContext>;
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
export { MarkdownProvider, useMarkdownContext };
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import type { GlobalShiki } from '@components/XMarkdownCore/hooks/useShiki';
|
|
||||||
import type { InitShikiOptions } from '../../shared';
|
|
||||||
import type { ElxRunCodeOptions } from '../RunCode/type';
|
|
||||||
|
|
||||||
export interface MarkdownContext {
|
|
||||||
// markdown 字符串内容
|
|
||||||
markdown?: string;
|
|
||||||
// 是否允许 HTML
|
|
||||||
allowHtml?: boolean;
|
|
||||||
// 是否启用代码行号
|
|
||||||
enableCodeLineNumber?: boolean;
|
|
||||||
// 是否启用 LaTeX 支持
|
|
||||||
enableLatex?: boolean;
|
|
||||||
// 是否开启动画
|
|
||||||
enableAnimate?: boolean;
|
|
||||||
// 是否启用换行符转 <br>
|
|
||||||
enableBreaks?: boolean;
|
|
||||||
// 自定义代码块渲染函数
|
|
||||||
codeXRender?: Record<string, any>;
|
|
||||||
// 自定义代码块插槽
|
|
||||||
codeXSlot?: Record<string, any>;
|
|
||||||
// 自定义代码块属性
|
|
||||||
codeXProps?: Record<string, any>;
|
|
||||||
// 自定义代码高亮主题
|
|
||||||
codeHighlightTheme?: builtinTheme;
|
|
||||||
// 自定义属性对象
|
|
||||||
customAttrs?: CustomAttrs;
|
|
||||||
// remark 插件列表
|
|
||||||
remarkPlugins?: PluggableList;
|
|
||||||
remarkPluginsAhead?: PluggableList;
|
|
||||||
// rehype 插件列表
|
|
||||||
rehypePlugins?: PluggableList;
|
|
||||||
rehypePluginsAhead?: PluggableList;
|
|
||||||
// rehype 配置项
|
|
||||||
rehypeOptions?: Record<string, any>;
|
|
||||||
// 是否启用内容清洗
|
|
||||||
sanitize?: boolean;
|
|
||||||
// 清洗选项
|
|
||||||
sanitizeOptions?: SanitizeOptions;
|
|
||||||
// Mermaid 配置对象
|
|
||||||
mermaidConfig?: Record<string, any>;
|
|
||||||
// 主题配置
|
|
||||||
themes?: InitShikiOptions['themes'];
|
|
||||||
// 默认主题模式
|
|
||||||
defaultThemeMode?: 'light' | 'dark';
|
|
||||||
// 是否是暗黑模式(代码高亮块)
|
|
||||||
isDarkMode?: boolean;
|
|
||||||
// 自定义当前主题下的代码颜色配置
|
|
||||||
colorReplacements?: InitShikiOptions['colorReplacements'];
|
|
||||||
// 是否显示查看代码按钮
|
|
||||||
needViewCodeBtn?: boolean;
|
|
||||||
// 是否是安全模式预览html
|
|
||||||
secureViewCode?: boolean;
|
|
||||||
// 预览代码弹窗部分配置
|
|
||||||
viewCodeModalOptions?: ElxRunCodeOptions;
|
|
||||||
// 全局shiki
|
|
||||||
globalShiki?: GlobalShiki;
|
|
||||||
}
|
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { MermaidToolbarConfig, MermaidToolbarEmits } from './types';
|
|
||||||
import {
|
|
||||||
Aim,
|
|
||||||
Check,
|
|
||||||
CopyDocument,
|
|
||||||
Download,
|
|
||||||
FullScreen,
|
|
||||||
ZoomIn,
|
|
||||||
ZoomOut
|
|
||||||
} from '@element-plus/icons-vue';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
interface MermaidToolbarInternalProps {
|
|
||||||
toolbarConfig?: MermaidToolbarConfig;
|
|
||||||
isSourceCodeMode?: boolean;
|
|
||||||
sourceCode?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<MermaidToolbarInternalProps>(), {
|
|
||||||
toolbarConfig: () => ({}),
|
|
||||||
isSourceCodeMode: false,
|
|
||||||
sourceCode: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<MermaidToolbarEmits>();
|
|
||||||
|
|
||||||
// 复制成功状态
|
|
||||||
const isCopySuccess = ref(false);
|
|
||||||
|
|
||||||
// 当前激活的 tab
|
|
||||||
const activeTab = computed({
|
|
||||||
get: () => (props.isSourceCodeMode ? 'code' : 'diagram'),
|
|
||||||
set: (value: string) => {
|
|
||||||
if (value === 'code' && !props.isSourceCodeMode) {
|
|
||||||
handleToggleCode();
|
|
||||||
} else if (value === 'diagram' && props.isSourceCodeMode) {
|
|
||||||
handleToggleCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 合并默认配置
|
|
||||||
const config = computed(() => {
|
|
||||||
return {
|
|
||||||
showToolbar: true,
|
|
||||||
showFullscreen: true,
|
|
||||||
showZoomIn: true,
|
|
||||||
showZoomOut: true,
|
|
||||||
showReset: true,
|
|
||||||
showDownload: true,
|
|
||||||
toolbarStyle: {},
|
|
||||||
toolbarClass: '',
|
|
||||||
iconColor: undefined,
|
|
||||||
tabTextColor: undefined,
|
|
||||||
hoverBackgroundColor: undefined,
|
|
||||||
tabActiveBackgroundColor: undefined,
|
|
||||||
...props.toolbarConfig
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 动态图标颜色
|
|
||||||
const iconColorStyle = computed(() => {
|
|
||||||
const style: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (config.value.iconColor) {
|
|
||||||
style.color = config.value.iconColor;
|
|
||||||
style['--custom-icon-color'] = config.value.iconColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置hover背景色
|
|
||||||
if (config.value.hoverBackgroundColor) {
|
|
||||||
style['--custom-hover-bg'] = config.value.hoverBackgroundColor;
|
|
||||||
} else if (config.value.iconColor) {
|
|
||||||
// 如果设置了图标颜色但没有设置hover背景色,使用稍暗的背景
|
|
||||||
style['--custom-hover-bg'] = 'rgba(0, 0, 0, 0.1)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return style;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 动态 tab 文字颜色
|
|
||||||
const tabTextColorStyle = computed(() => {
|
|
||||||
const style: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (config.value.tabTextColor) {
|
|
||||||
style['--tab-text-color'] = config.value.tabTextColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置tab激活状态背景色
|
|
||||||
if (config.value.tabActiveBackgroundColor) {
|
|
||||||
style['--tab-active-bg'] = config.value.tabActiveBackgroundColor;
|
|
||||||
} else if (config.value.tabTextColor) {
|
|
||||||
// 如果设置了文字颜色但没有设置激活背景色,使用稍暗的背景
|
|
||||||
style['--tab-active-bg'] = 'rgba(0, 0, 0, 0.1)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return style;
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleZoomIn(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
emit('onZoomIn');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleZoomOut(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
emit('onZoomOut');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleReset(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
emit('onReset');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFullscreen(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
emit('onFullscreen');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToggleCode(event?: Event) {
|
|
||||||
if (event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
emit('onToggleCode');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDownload(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
emit('onDownload');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCopyCode(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// 如果正在显示成功状态,不执行复制操作
|
|
||||||
if (isCopySuccess.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!props.sourceCode) {
|
|
||||||
emit('onCopyCode');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用现代剪贴板 API
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(props.sourceCode);
|
|
||||||
} else {
|
|
||||||
// 降级方案:使用传统方法
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = props.sourceCode;
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
textArea.style.left = '-999999px';
|
|
||||||
textArea.style.top = '-999999px';
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
textArea.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置复制成功状态
|
|
||||||
isCopySuccess.value = true;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
isCopySuccess.value = false;
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
emit('onCopyCode');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy code: ', err);
|
|
||||||
// 如果复制失败,也通知父组件,让父组件决定如何处理
|
|
||||||
emit('onCopyCode');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToolbarClick(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTabClick(tabName: string) {
|
|
||||||
activeTab.value = tabName;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabClickEvent {
|
|
||||||
paneName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTabClickEvent(pane: TabClickEvent) {
|
|
||||||
handleTabClick(pane.paneName);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<!-- 正常状态:显示工具栏 -->
|
|
||||||
<div
|
|
||||||
v-if="config.showToolbar"
|
|
||||||
class="mermaid-toolbar"
|
|
||||||
:class="config.toolbarClass"
|
|
||||||
:style="config.toolbarStyle"
|
|
||||||
@click="handleToolbarClick"
|
|
||||||
>
|
|
||||||
<!-- 左侧 Tabs -->
|
|
||||||
<div class="toolbar-left" :style="tabTextColorStyle">
|
|
||||||
<el-tabs
|
|
||||||
:model-value="activeTab"
|
|
||||||
class="toolbar-tabs"
|
|
||||||
@tab-click="handleTabClickEvent"
|
|
||||||
>
|
|
||||||
<el-tab-pane label="图片" name="diagram" />
|
|
||||||
<el-tab-pane label="代码" name="code" />
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧按钮组 -->
|
|
||||||
<div class="toolbar-right">
|
|
||||||
<!-- 代码视图:只显示复制按钮 -->
|
|
||||||
<template v-if="props.isSourceCodeMode">
|
|
||||||
<div
|
|
||||||
class="toolbar-action-btn"
|
|
||||||
:class="{ 'copy-success': isCopySuccess }"
|
|
||||||
:style="iconColorStyle"
|
|
||||||
@click="handleCopyCode($event)"
|
|
||||||
>
|
|
||||||
<el-icon :size="16">
|
|
||||||
<Check v-if="isCopySuccess" />
|
|
||||||
<CopyDocument v-else />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 图片视图:显示所有操作按钮 -->
|
|
||||||
<template v-else>
|
|
||||||
<!-- 下载按钮 -->
|
|
||||||
<div
|
|
||||||
v-if="config.showDownload"
|
|
||||||
class="toolbar-action-btn"
|
|
||||||
:style="iconColorStyle"
|
|
||||||
@click="handleDownload($event)"
|
|
||||||
>
|
|
||||||
<el-icon :size="16">
|
|
||||||
<Download />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分割线 -->
|
|
||||||
<div v-if="config.showDownload" class="toolbar-divider" />
|
|
||||||
|
|
||||||
<!-- 缩小按钮 -->
|
|
||||||
<div
|
|
||||||
v-if="config.showZoomOut"
|
|
||||||
class="toolbar-action-btn"
|
|
||||||
:style="iconColorStyle"
|
|
||||||
@click="handleZoomOut($event)"
|
|
||||||
>
|
|
||||||
<el-icon :size="16">
|
|
||||||
<ZoomOut />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 放大按钮 -->
|
|
||||||
<div
|
|
||||||
v-if="config.showZoomIn"
|
|
||||||
class="toolbar-action-btn"
|
|
||||||
:style="iconColorStyle"
|
|
||||||
@click="handleZoomIn($event)"
|
|
||||||
>
|
|
||||||
<el-icon :size="16">
|
|
||||||
<ZoomIn />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 适应按钮 (重置) -->
|
|
||||||
<div
|
|
||||||
v-if="config.showReset"
|
|
||||||
class="toolbar-action-btn"
|
|
||||||
:style="iconColorStyle"
|
|
||||||
@click="handleReset($event)"
|
|
||||||
>
|
|
||||||
<el-icon :size="16">
|
|
||||||
<Aim />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 全屏按钮 -->
|
|
||||||
<div
|
|
||||||
v-if="config.showFullscreen"
|
|
||||||
class="toolbar-action-btn"
|
|
||||||
:style="iconColorStyle"
|
|
||||||
@click="handleFullscreen($event)"
|
|
||||||
>
|
|
||||||
<el-icon :size="16">
|
|
||||||
<FullScreen />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.mermaid-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 42px;
|
|
||||||
background: #ebecef;
|
|
||||||
border-radius: 3px 3px 0 0;
|
|
||||||
padding: 0 12px;
|
|
||||||
pointer-events: auto;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.toolbar-left {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.toolbar-tabs {
|
|
||||||
:deep(.el-tabs__header) {
|
|
||||||
margin: 0;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tabs__nav) {
|
|
||||||
background: #dddee1;
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tabs__nav-wrap) {
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tabs__item) {
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
border: none;
|
|
||||||
color: var(--tab-text-color, var(--el-text-color-regular));
|
|
||||||
width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-weight: 700;
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
color: var(--tab-text-color, var(--el-text-color-primary));
|
|
||||||
background: var(--tab-active-bg, rgba(255, 255, 255, 0.8));
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(.is-active) {
|
|
||||||
color: var(--tab-text-color, var(--el-text-color-primary));
|
|
||||||
background: #d1d2d5;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tabs__active-bar) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0;
|
|
||||||
|
|
||||||
.toolbar-action-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
border-radius: 4px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover:not(.disabled) {
|
|
||||||
color: var(--custom-icon-color, var(--el-text-color-primary));
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
background: #dddee1;
|
|
||||||
border-radius: 4px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(.disabled) {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none !important;
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.toolbar-action-btn-last {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.copy-success {
|
|
||||||
cursor: default;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-divider {
|
|
||||||
width: 1px;
|
|
||||||
height: 16px;
|
|
||||||
background: var(--el-border-color);
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 父容器悬停时显示工具栏 */
|
|
||||||
:global(.markdown-mermaid:hover .mermaid-toolbar) {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 全屏状态下的样式调整 */
|
|
||||||
:global(.markdown-mermaid:fullscreen .mermaid-toolbar) {
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
.toolbar-left .toolbar-tabs {
|
|
||||||
:deep(.el-tabs__item) {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
color: white;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(.is-active) {
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-right {
|
|
||||||
.toolbar-action-btn {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
|
|
||||||
&:hover:not(.disabled) {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-divider {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// 复制到剪贴板
|
|
||||||
export async function copyToClipboard(content: string): Promise<boolean> {
|
|
||||||
if (!content)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(content);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = content;
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
textArea.style.left = '-999999px';
|
|
||||||
textArea.style.top = '-999999px';
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
textArea.remove();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error('复制失败: ', err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SVG下载功能
|
|
||||||
export function downloadSvgAsPng(svg: string): void {
|
|
||||||
if (!svg)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const svgDataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
||||||
const img = new Image();
|
|
||||||
|
|
||||||
img.onload = () => {
|
|
||||||
try {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: false });
|
|
||||||
if (!ctx)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const scale = 2;
|
|
||||||
canvas.width = img.width * scale;
|
|
||||||
canvas.height = img.height * scale;
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
// 白色背景
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
// 绘制SVG
|
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
// 下载
|
|
||||||
const timestamp = new Date()
|
|
||||||
.toISOString()
|
|
||||||
.slice(0, 19)
|
|
||||||
.replace(/:/g, '-');
|
|
||||||
|
|
||||||
try {
|
|
||||||
canvas.toBlob(
|
|
||||||
blob => {
|
|
||||||
if (!blob)
|
|
||||||
return;
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `mermaid-diagram-${timestamp}.png`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
},
|
|
||||||
'image/png',
|
|
||||||
0.95
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (toBlobError) {
|
|
||||||
console.error('toBlobError:', toBlobError);
|
|
||||||
// 降级方案
|
|
||||||
try {
|
|
||||||
const dataUrl = canvas.toDataURL('image/png', 0.95);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = dataUrl;
|
|
||||||
link.download = `mermaid-diagram-${timestamp}.png`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
}
|
|
||||||
catch (dataUrlError) {
|
|
||||||
console.error('dataUrlError:', dataUrlError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (canvasError) {
|
|
||||||
console.error('Canvas操作失败:', canvasError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
img.onerror = error => {
|
|
||||||
console.error('Image load error:', error);
|
|
||||||
};
|
|
||||||
|
|
||||||
img.src = svgDataUrl;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('下载失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { MdComponent } from '../types';
|
|
||||||
import type { MermaidExposeProps, MermaidToolbarConfig } from './types';
|
|
||||||
import { debounce } from 'radash';
|
|
||||||
import { computed, nextTick, ref, toValue, watch } from 'vue';
|
|
||||||
import { useMermaid, useMermaidZoom } from '../../hooks';
|
|
||||||
import { useMarkdownContext } from '../MarkdownProvider';
|
|
||||||
import { copyToClipboard, downloadSvgAsPng } from './composables';
|
|
||||||
import MermaidToolbar from './MermaidToolbar.vue';
|
|
||||||
|
|
||||||
interface MermaidProps extends MdComponent {
|
|
||||||
toolbarConfig?: MermaidToolbarConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<MermaidProps>(), {
|
|
||||||
raw: () => ({}),
|
|
||||||
toolbarConfig: () => ({})
|
|
||||||
});
|
|
||||||
|
|
||||||
const mermaidContent = computed(() => props.raw?.content || '');
|
|
||||||
const mermaidResult = useMermaid(mermaidContent, {
|
|
||||||
id: `mermaid-${props.raw?.key || 'default'}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const svg = ref('');
|
|
||||||
const isLoading = computed(
|
|
||||||
() => !mermaidResult.data.value && !mermaidResult.error.value
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取插槽上下文
|
|
||||||
const context = useMarkdownContext();
|
|
||||||
const { codeXSlot } = toValue(context);
|
|
||||||
|
|
||||||
// 计算工具栏配置,合并默认值
|
|
||||||
const toolbarConfig = computed(() => {
|
|
||||||
const contextMermaidConfig = toValue(context)?.mermaidConfig || {};
|
|
||||||
return {
|
|
||||||
showToolbar: true,
|
|
||||||
showFullscreen: true,
|
|
||||||
showZoomIn: true,
|
|
||||||
showZoomOut: true,
|
|
||||||
showReset: true,
|
|
||||||
...contextMermaidConfig,
|
|
||||||
...props.toolbarConfig
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
|
||||||
const showSourceCode = ref(false);
|
|
||||||
|
|
||||||
// 初始化缩放功能
|
|
||||||
const zoomControls = useMermaidZoom({
|
|
||||||
container: containerRef,
|
|
||||||
scaleStep: 0.2,
|
|
||||||
minScale: 0.1,
|
|
||||||
maxScale: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
const debouncedInitialize = debounce({ delay: 500 }, onContentTransitionEnter);
|
|
||||||
watch(
|
|
||||||
() => mermaidResult.data.value,
|
|
||||||
newSvg => {
|
|
||||||
if (newSvg) {
|
|
||||||
svg.value = newSvg;
|
|
||||||
debouncedInitialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
watch(svg, newSvg => {
|
|
||||||
if (newSvg) {
|
|
||||||
debouncedInitialize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 工具栏事件处理
|
|
||||||
function handleZoomIn() {
|
|
||||||
if (!showSourceCode.value) {
|
|
||||||
zoomControls?.zoomIn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleZoomOut() {
|
|
||||||
if (!showSourceCode.value) {
|
|
||||||
zoomControls?.zoomOut();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleReset() {
|
|
||||||
if (!showSourceCode.value) {
|
|
||||||
zoomControls?.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFullscreen() {
|
|
||||||
if (!showSourceCode.value) {
|
|
||||||
zoomControls?.fullscreen();
|
|
||||||
zoomControls?.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToggleCode() {
|
|
||||||
showSourceCode.value = !showSourceCode.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCopyCode() {
|
|
||||||
if (!props.raw.content) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
copyToClipboard(props.raw.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDownload() {
|
|
||||||
downloadSvgAsPng(svg.value);
|
|
||||||
}
|
|
||||||
// 处理图表内容过渡完成事件
|
|
||||||
function onContentTransitionEnter() {
|
|
||||||
// 只在图表模式下初始化缩放功能
|
|
||||||
if (!showSourceCode.value) {
|
|
||||||
// 使用 nextTick 确保 DOM 完全更新
|
|
||||||
nextTick(() => {
|
|
||||||
if (containerRef.value) {
|
|
||||||
zoomControls.initialize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建暴露给插槽的方法对象
|
|
||||||
const exposedMethods = computed(() => {
|
|
||||||
return {
|
|
||||||
// 基础属性
|
|
||||||
showSourceCode: showSourceCode.value,
|
|
||||||
svg: svg.value,
|
|
||||||
rawContent: props.raw.content || '',
|
|
||||||
toolbarConfig: toolbarConfig.value,
|
|
||||||
isLoading: isLoading.value,
|
|
||||||
|
|
||||||
// 缩放控制方法
|
|
||||||
zoomIn: handleZoomIn,
|
|
||||||
zoomOut: handleZoomOut,
|
|
||||||
reset: handleReset,
|
|
||||||
fullscreen: handleFullscreen,
|
|
||||||
|
|
||||||
// 其他操作方法
|
|
||||||
toggleCode: handleToggleCode,
|
|
||||||
copyCode: handleCopyCode,
|
|
||||||
download: handleDownload,
|
|
||||||
|
|
||||||
// 原始 props(除了重复的 toolbarConfig)
|
|
||||||
raw: props.raw
|
|
||||||
} satisfies MermaidExposeProps;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="containerRef" :key="props.raw.key" class="markdown-mermaid">
|
|
||||||
<!-- 工具栏 -->
|
|
||||||
<Transition name="toolbar" appear>
|
|
||||||
<div class="toolbar-container">
|
|
||||||
<!-- 自定义完整头部插槽 -->
|
|
||||||
<component
|
|
||||||
:is="codeXSlot.codeMermaidHeader"
|
|
||||||
v-if="codeXSlot?.codeMermaidHeader"
|
|
||||||
v-bind="exposedMethods"
|
|
||||||
/>
|
|
||||||
<!-- 默认工具栏 + 自定义操作插槽 -->
|
|
||||||
<template v-else>
|
|
||||||
<!-- 自定义操作按钮插槽 -->
|
|
||||||
<component
|
|
||||||
:is="codeXSlot.codeMermaidHeaderControl"
|
|
||||||
v-if="codeXSlot?.codeMermaidHeaderControl"
|
|
||||||
v-bind="exposedMethods"
|
|
||||||
/>
|
|
||||||
<!-- 默认工具栏 -->
|
|
||||||
<MermaidToolbar
|
|
||||||
v-else
|
|
||||||
:toolbar-config="toolbarConfig"
|
|
||||||
:is-source-code-mode="showSourceCode"
|
|
||||||
:source-code="props.raw.content"
|
|
||||||
@on-zoom-in="handleZoomIn"
|
|
||||||
@on-zoom-out="handleZoomOut"
|
|
||||||
@on-reset="handleReset"
|
|
||||||
@on-fullscreen="handleFullscreen"
|
|
||||||
@on-toggle-code="handleToggleCode"
|
|
||||||
@on-copy-code="handleCopyCode"
|
|
||||||
@on-download="handleDownload"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
<Transition
|
|
||||||
name="content"
|
|
||||||
mode="out-in"
|
|
||||||
@after-enter="onContentTransitionEnter"
|
|
||||||
>
|
|
||||||
<pre v-if="showSourceCode" key="source" class="mermaid-source-code">{{
|
|
||||||
props.raw.content
|
|
||||||
}}</pre>
|
|
||||||
<div v-else class="mermaid-content" v-html="svg" />
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style src="./style.scss"></style>
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
.markdown-mermaid {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 100px;
|
|
||||||
min-height: 100px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
// 工具栏容器样式
|
|
||||||
.toolbar-container {
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: white;
|
|
||||||
|
|
||||||
// 确保自定义头部插槽正确显示
|
|
||||||
.custom-mermaid-header {
|
|
||||||
position: relative;
|
|
||||||
z-index: 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认工具栏的基础样式
|
|
||||||
.mermaid-language-tag {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #f0f9ff;
|
|
||||||
border: 1px solid #e0f2fe;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #0891b2;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简单默认工具栏样式
|
|
||||||
.mermaid-default-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: white;
|
|
||||||
|
|
||||||
.toolbar-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
color: #374151;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f3f4f6;
|
|
||||||
border-color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 200px;
|
|
||||||
// max-height: 80vh;
|
|
||||||
cursor: grab;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
overflow: hidden; // 防止未缩放的大图表溢出
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
transform-origin: center center; // SVG 的变换原点
|
|
||||||
position: relative;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染状态时的加载效果
|
|
||||||
&.rendering {
|
|
||||||
svg {
|
|
||||||
opacity: 0.8;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全屏
|
|
||||||
&:fullscreen {
|
|
||||||
.mermaid-content {
|
|
||||||
max-height: 100vh;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dragging {
|
|
||||||
.mermaid-content {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.zoom-limit {
|
|
||||||
.mermaid-content {
|
|
||||||
transform-origin: center center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-source-code {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #333;
|
|
||||||
overflow: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容切换过渡 - 减少闪烁,优化为淡入淡出
|
|
||||||
.content-enter-active {
|
|
||||||
transition: opacity 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-leave-active {
|
|
||||||
transition: opacity 0.15s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具栏过渡
|
|
||||||
.toolbar-enter-active,
|
|
||||||
.toolbar-leave-active {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
export interface MermaidToolbarConfig {
|
|
||||||
showToolbar?: boolean;
|
|
||||||
showFullscreen?: boolean;
|
|
||||||
showZoomIn?: boolean;
|
|
||||||
showZoomOut?: boolean;
|
|
||||||
showReset?: boolean;
|
|
||||||
showDownload?: boolean;
|
|
||||||
toolbarStyle?: Record<string, any>;
|
|
||||||
toolbarClass?: string;
|
|
||||||
iconColor?: string;
|
|
||||||
tabTextColor?: string;
|
|
||||||
hoverBackgroundColor?: string;
|
|
||||||
tabActiveBackgroundColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MermaidToolbarProps extends MermaidToolbarConfig {}
|
|
||||||
|
|
||||||
export interface MermaidZoomControls {
|
|
||||||
zoomIn: () => void;
|
|
||||||
zoomOut: () => void;
|
|
||||||
reset: () => void;
|
|
||||||
fullscreen: () => void;
|
|
||||||
destroy: () => void;
|
|
||||||
initialize: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseMermaidZoomOptions {
|
|
||||||
container: Ref<HTMLElement | null>;
|
|
||||||
scaleStep?: number;
|
|
||||||
minScale?: number;
|
|
||||||
maxScale?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MermaidToolbarEmits {
|
|
||||||
onZoomIn: [];
|
|
||||||
onZoomOut: [];
|
|
||||||
onReset: [];
|
|
||||||
onFullscreen: [];
|
|
||||||
onEdit: [];
|
|
||||||
onToggleCode: [];
|
|
||||||
onCopyCode: [];
|
|
||||||
onDownload: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mermaid 组件暴露给插槽的方法接口
|
|
||||||
export interface MermaidExposedMethods {
|
|
||||||
zoomIn: () => void;
|
|
||||||
zoomOut: () => void;
|
|
||||||
reset: () => void;
|
|
||||||
fullscreen: () => void;
|
|
||||||
toggleCode: () => void;
|
|
||||||
copyCode: () => void;
|
|
||||||
download: () => void;
|
|
||||||
svg: import('vue').Ref<string>;
|
|
||||||
showSourceCode: import('vue').Ref<boolean>;
|
|
||||||
toolbarConfig: import('vue').ComputedRef<MermaidToolbarConfig>;
|
|
||||||
rawContent: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MermaidExposeProps {
|
|
||||||
showSourceCode: boolean;
|
|
||||||
svg: string;
|
|
||||||
rawContent: any;
|
|
||||||
toolbarConfig: MermaidToolbarConfig;
|
|
||||||
isLoading: boolean;
|
|
||||||
|
|
||||||
// 缩放控制方法
|
|
||||||
zoomIn: () => void;
|
|
||||||
zoomOut: () => void;
|
|
||||||
reset: () => void;
|
|
||||||
fullscreen: () => void;
|
|
||||||
|
|
||||||
// 其他操作方法
|
|
||||||
toggleCode: () => void;
|
|
||||||
copyCode: () => Promise<void>;
|
|
||||||
download: () => void;
|
|
||||||
|
|
||||||
// 原始 props(除了重复的 toolbarConfig)
|
|
||||||
raw: any;
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<script lang="ts" setup></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="dot-spinner">
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
<div class="dot-spinner__dot" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.dot-spinner {
|
|
||||||
--uib-size: 2.8rem;
|
|
||||||
--uib-speed: 0.9s;
|
|
||||||
--uib-color: var(--el-color-primary);
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: var(--uib-size);
|
|
||||||
width: var(--uib-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot::before {
|
|
||||||
content: '';
|
|
||||||
height: 20%;
|
|
||||||
width: 20%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--uib-color);
|
|
||||||
transform: scale(0);
|
|
||||||
opacity: 0.5;
|
|
||||||
animation: pulse0112 calc(var(--uib-speed) * 1.311) ease-in-out infinite;
|
|
||||||
box-shadow: 0 0 20px rgba(18, 31, 53, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(2) {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(2)::before {
|
|
||||||
animation-delay: calc(var(--uib-speed) * -0.875);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(3) {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(3)::before {
|
|
||||||
animation-delay: calc(var(--uib-speed) * -0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(4) {
|
|
||||||
transform: rotate(135deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(4)::before {
|
|
||||||
animation-delay: calc(var(--uib-speed) * -0.625);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(5) {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(5)::before {
|
|
||||||
animation-delay: calc(var(--uib-speed) * -0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(6) {
|
|
||||||
transform: rotate(225deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(6)::before {
|
|
||||||
animation-delay: calc(var(--uib-speed) * -0.375);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(7) {
|
|
||||||
transform: rotate(270deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(7)::before {
|
|
||||||
animation-delay: calc(var(--uib-speed) * -0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(8) {
|
|
||||||
transform: rotate(315deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-spinner__dot:nth-child(8)::before {
|
|
||||||
animation-delay: calc(var(--uib-speed) * -0.125);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse0112 {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(0);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export enum SELECT_OPTIONS_ENUM {
|
|
||||||
'CODE' = '代码',
|
|
||||||
'VIEW' = '预览'
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { ElxRunCodeContentProps } from '../type';
|
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
|
||||||
import HighLightCode from '../../HighLightCode/index.vue';
|
|
||||||
import { useMarkdownContext } from '../../MarkdownProvider';
|
|
||||||
import CustomLoading from './custom-loading.vue';
|
|
||||||
import { SELECT_OPTIONS_ENUM } from './options';
|
|
||||||
|
|
||||||
const props = defineProps<ElxRunCodeContentProps>();
|
|
||||||
|
|
||||||
const computedClass = computed(() => `pre-md ${props.preClass}`);
|
|
||||||
|
|
||||||
const codeClass = computed(() => `language-${props.lang || 'text'}`);
|
|
||||||
|
|
||||||
const iframeRef = ref<HTMLIFrameElement>();
|
|
||||||
|
|
||||||
const allHtml = computed(() => props.content);
|
|
||||||
|
|
||||||
const codeContainerRef = ref<HTMLElement>();
|
|
||||||
|
|
||||||
const isLoading = ref(false);
|
|
||||||
|
|
||||||
const context = useMarkdownContext();
|
|
||||||
|
|
||||||
const isSafeViewCode = computed(() => {
|
|
||||||
return context.value.secureViewCode;
|
|
||||||
});
|
|
||||||
const enableCodeLineNumber = computed(() => {
|
|
||||||
return context.value?.enableCodeLineNumber || false;
|
|
||||||
});
|
|
||||||
function doRenderIframe() {
|
|
||||||
const iframe = iframeRef.value;
|
|
||||||
if (!iframe)
|
|
||||||
return;
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
const rawHtml = allHtml.value || '';
|
|
||||||
let sanitizedHtml = rawHtml;
|
|
||||||
|
|
||||||
// 安全模式过滤
|
|
||||||
if (isSafeViewCode.value) {
|
|
||||||
sanitizedHtml = DOMPurify.sanitize(rawHtml, {
|
|
||||||
WHOLE_DOCUMENT: true,
|
|
||||||
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
|
|
||||||
FORBID_ATTR: ['onerror', 'onclick', 'onload', 'style']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 <head> 中是否有 UTF-8 charset
|
|
||||||
let finalHtml = sanitizedHtml;
|
|
||||||
const hasHead = /<head[^>]*>/i.test(sanitizedHtml);
|
|
||||||
const hasUtf8Meta =
|
|
||||||
/<head[^>]*>[\s\S]*?<meta\s[^>]*charset=["']?utf-8["']?/i.test(
|
|
||||||
sanitizedHtml
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasHead) {
|
|
||||||
if (!hasUtf8Meta) {
|
|
||||||
finalHtml = sanitizedHtml.replace(
|
|
||||||
/<head[^>]*>/i,
|
|
||||||
match => `${match}<meta charset="UTF-8">`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 没有 <head>,插入 <head><meta charset="UTF-8"></head> 到 <html> 或最前
|
|
||||||
if (/<html[^>]*>/i.test(sanitizedHtml)) {
|
|
||||||
finalHtml = sanitizedHtml.replace(
|
|
||||||
/<html[^>]*>/i,
|
|
||||||
match => `${match}<head><meta charset="UTF-8"></head>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 甚至没有 <html>,包一层完整结构
|
|
||||||
finalHtml = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head><meta charset="UTF-8"></head>
|
|
||||||
<body>${sanitizedHtml}</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([finalHtml], { type: 'text/html' });
|
|
||||||
|
|
||||||
if (iframe.src && iframe.src.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(iframe.src);
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe.src = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const onLoad = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
isLoading.value = false;
|
|
||||||
}, 300);
|
|
||||||
iframe.removeEventListener('load', onLoad);
|
|
||||||
};
|
|
||||||
iframe.addEventListener('load', onLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderIframe = _.debounce(() => {
|
|
||||||
doRenderIframe();
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
function startRender() {
|
|
||||||
if (props.nowView === SELECT_OPTIONS_ENUM.VIEW) {
|
|
||||||
isLoading.value = true;
|
|
||||||
renderIframe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [props.nowView, isSafeViewCode.value],
|
|
||||||
() => {
|
|
||||||
startRender();
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.code,
|
|
||||||
() => {
|
|
||||||
startRender();
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
nextTick(() => {
|
|
||||||
startRender();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<el-scrollbar
|
|
||||||
ref="codeContainerRef"
|
|
||||||
class="elx-run-code-content-scrollbar"
|
|
||||||
:style="preStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-show="props.nowView === SELECT_OPTIONS_ENUM.CODE"
|
|
||||||
class="elx-xmarkdown-container elx-run-code-content"
|
|
||||||
>
|
|
||||||
<pre>
|
|
||||||
<div
|
|
||||||
:class="computedClass"
|
|
||||||
:style="preStyle"
|
|
||||||
>
|
|
||||||
<code
|
|
||||||
class="elx-run-code-content-code"
|
|
||||||
:class="codeClass"
|
|
||||||
>
|
|
||||||
<HighLightCode
|
|
||||||
:enable-code-line-number="enableCodeLineNumber"
|
|
||||||
:lang="props.lang"
|
|
||||||
:code="props.code"
|
|
||||||
/>
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="props.nowView === SELECT_OPTIONS_ENUM.VIEW"
|
|
||||||
style="position: relative; width: 100%; height: 100%"
|
|
||||||
class="elx-run-code-content-view"
|
|
||||||
>
|
|
||||||
<div v-if="isLoading" class="iframe-loading-mask">
|
|
||||||
<CustomLoading />
|
|
||||||
</div>
|
|
||||||
<div v-show="!isLoading" class="elx-run-code-content-view-iframe">
|
|
||||||
<iframe
|
|
||||||
ref="iframeRef"
|
|
||||||
sandbox="allow-scripts"
|
|
||||||
style="border: 0; width: 100%; height: 79.5vh"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style src="./style/index.scss"></style>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { ElxRunCodeHeaderTypes } from '../type';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { SELECT_OPTIONS_ENUM } from './options';
|
|
||||||
|
|
||||||
interface ElxRunCodeProps {
|
|
||||||
value: ElxRunCodeHeaderTypes['options'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ElxRunCodeProps>(), {});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'changeSelect', val: string): void;
|
|
||||||
(e: 'update:value'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const options = Object.values(SELECT_OPTIONS_ENUM);
|
|
||||||
|
|
||||||
const selectValue = useVModel(props, 'value', emit);
|
|
||||||
|
|
||||||
function change(val: string) {
|
|
||||||
emit('changeSelect', val);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="custom-style">
|
|
||||||
<el-segmented v-model="selectValue" :options="options" @change="change" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style src="./style/index.scss"></style>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
.custom-style .el-segmented {
|
|
||||||
--el-segmented-item-selected-color: white;
|
|
||||||
--el-border-radius-base: var(--shiki-custom-brr);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark {
|
|
||||||
.custom-style .el-segmented {
|
|
||||||
--el-segmented-item-selected-bg-color: #409eff;
|
|
||||||
--el-segmented-item-selected-color: white;
|
|
||||||
--el-segmented-item-hover-bg-color: #4e4e4e;
|
|
||||||
--el-segmented-item-active-bg-color: #4e4e4e;
|
|
||||||
--el-fill-color-light: #39393a;
|
|
||||||
.el-segmented__item-label {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.elx-run-code-content-scrollbar {
|
|
||||||
background-color: var(--shiki-dark-bg) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.elx-run-code-content-scrollbar {
|
|
||||||
background-color: var(--shiki-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.elx-run-code-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 80vh !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
.elx-run-code-content-code {
|
|
||||||
overflow: visible !important;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
white-space: nowrap !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
pre div.pre-md {
|
|
||||||
width: 100%;
|
|
||||||
height: 100% !important;
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.iframe-loading-mask {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 79.8vh;
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { ElxRunCodeHeaderTypes, ElxRunCodeProps } from './type';
|
|
||||||
import { CloseBold } from '@element-plus/icons-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { computed, h, ref, toValue } from 'vue';
|
|
||||||
import { useMarkdownContext } from '../MarkdownProvider';
|
|
||||||
import { SELECT_OPTIONS_ENUM } from './components/options';
|
|
||||||
import RunCodeContent from './components/run-code-content.vue';
|
|
||||||
import RunCodeHeader from './components/run-code-header.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ElxRunCodeProps>(), {
|
|
||||||
code: () => [],
|
|
||||||
lang: '',
|
|
||||||
mode: 'drawer'
|
|
||||||
});
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:visible'): void;
|
|
||||||
}>();
|
|
||||||
const drawer = useVModel(props, 'visible', emit);
|
|
||||||
|
|
||||||
const selectValue = ref<ElxRunCodeHeaderTypes['options']>(
|
|
||||||
SELECT_OPTIONS_ENUM.VIEW
|
|
||||||
);
|
|
||||||
|
|
||||||
const isView = computed(() => selectValue.value === SELECT_OPTIONS_ENUM.VIEW);
|
|
||||||
const context = useMarkdownContext();
|
|
||||||
const { codeXSlot } = toValue(context) || {};
|
|
||||||
|
|
||||||
function changeSelectValue(val: ElxRunCodeHeaderTypes['options']) {
|
|
||||||
selectValue.value = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
drawer.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染插槽函数
|
|
||||||
function renderSlot(slotName: string) {
|
|
||||||
if (!codeXSlot) {
|
|
||||||
return 'div';
|
|
||||||
}
|
|
||||||
const slotFn = codeXSlot[slotName];
|
|
||||||
if (typeof slotFn === 'function') {
|
|
||||||
return slotFn({
|
|
||||||
...props,
|
|
||||||
value: selectValue.value,
|
|
||||||
close,
|
|
||||||
changeSelectValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(slotFn as any, {
|
|
||||||
...props,
|
|
||||||
value: selectValue.value,
|
|
||||||
close,
|
|
||||||
changeSelectValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const RunCodeCloseBtnComputed = computed(() => {
|
|
||||||
if (codeXSlot?.viewCodeCloseBtn) {
|
|
||||||
return renderSlot('viewCodeCloseBtn');
|
|
||||||
}
|
|
||||||
return CloseBold;
|
|
||||||
});
|
|
||||||
const RunCodeHeaderComputed = computed(() => {
|
|
||||||
if (codeXSlot?.viewCodeHeader) {
|
|
||||||
return renderSlot('viewCodeHeader');
|
|
||||||
}
|
|
||||||
return RunCodeHeader;
|
|
||||||
});
|
|
||||||
|
|
||||||
const RunCodeContentComputed = computed(() => {
|
|
||||||
if (codeXSlot?.viewCodeContent) {
|
|
||||||
return renderSlot('viewCodeContent');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-if="props.mode === 'dialog'"
|
|
||||||
v-model="drawer"
|
|
||||||
:class="`${props.customClass} ${isView ? 'elx-run-code-dialog-view' : ''}`"
|
|
||||||
:close-on-click-modal="props.dialogOptions?.closeOnClickModal ?? true"
|
|
||||||
:close-on-press-escape="props.dialogOptions?.closeOnPressEscape ?? true"
|
|
||||||
:show-close="false"
|
|
||||||
class="elx-run-code-dialog"
|
|
||||||
align-center
|
|
||||||
destroy-on-close
|
|
||||||
append-to-body
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<component :is="RunCodeHeaderComputed" v-model:value="selectValue" />
|
|
||||||
<el-button class="view-code-close-btn" @click="close">
|
|
||||||
<component :is="RunCodeCloseBtnComputed" />
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default>
|
|
||||||
<component :is="RunCodeContentComputed" v-if="RunCodeContentComputed" />
|
|
||||||
<RunCodeContent v-else v-bind="props" :now-view="selectValue" />
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
<el-drawer
|
|
||||||
v-if="props.mode === 'drawer'"
|
|
||||||
v-model="drawer"
|
|
||||||
:class="`${props.customClass} ${isView ? 'elx-run-code-drawer-view' : ''}`"
|
|
||||||
:close-on-click-modal="props.drawerOptions?.closeOnClickModal ?? true"
|
|
||||||
:close-on-press-escape="props.drawerOptions?.closeOnPressEscape ?? true"
|
|
||||||
:show-close="false"
|
|
||||||
class="elx-run-code-drawer"
|
|
||||||
align-center
|
|
||||||
destroy-on-close
|
|
||||||
append-to-body
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<component :is="RunCodeHeaderComputed" v-model:value="selectValue" />
|
|
||||||
<el-button
|
|
||||||
class="view-code-close-btn"
|
|
||||||
:class="{ customCloseBtn: !!codeXSlot?.viewCodeCloseBtn }"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
<component :is="RunCodeCloseBtnComputed" />
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default>
|
|
||||||
<component :is="RunCodeContentComputed" v-if="RunCodeContentComputed" />
|
|
||||||
<RunCodeContent v-else v-bind="props" :now-view="selectValue" />
|
|
||||||
</template>
|
|
||||||
</el-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style src="./style.scss"></style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
.elx-run-code-dialog,
|
|
||||||
.elx-run-code-drawer {
|
|
||||||
width: 75% !important;
|
|
||||||
background-color: var(--shiki-code-header-bg) !important;
|
|
||||||
.el-dialog__body {
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: var(--shiki-custom-brr);
|
|
||||||
}
|
|
||||||
.el-drawer__body {
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: var(--shiki-custom-brr);
|
|
||||||
}
|
|
||||||
.el-drawer__header {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
.el-dialog__headerbtn,
|
|
||||||
.el-drawer__close-btn {
|
|
||||||
color: var(--shiki-code-header-span-color);
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
right: 10px;
|
|
||||||
top: 15px;
|
|
||||||
font-size: 20px;
|
|
||||||
border-radius: var(--shiki-custom-brr);
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--shiki-code-header-btn-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-code-close-btn {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
span {
|
|
||||||
color: var(--shiki-code-header-span-color);
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
&:hover {
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
position: absolute;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
right: 10px;
|
|
||||||
top: 15px;
|
|
||||||
font-size: 20px;
|
|
||||||
border-radius: var(--shiki-custom-brr);
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--shiki-code-header-btn-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.customCloseBtn {
|
|
||||||
&:hover {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 媒体查询
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.elx-run-code-dialog,
|
|
||||||
.elx-run-code-drawer {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.elx-run-code-content-view-iframe {
|
|
||||||
height: 713px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.elx-run-code-dialog-view {
|
|
||||||
.el-dialog__body {
|
|
||||||
border: 1px solid transparent !important;
|
|
||||||
}
|
|
||||||
.el-drawer__body {
|
|
||||||
border: 1px solid transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import type { SELECT_OPTIONS_ENUM } from './components/options';
|
|
||||||
|
|
||||||
export interface ElxRunCodeHeaderTypes {
|
|
||||||
/**
|
|
||||||
* 视图 code 代码 view 预览
|
|
||||||
*/
|
|
||||||
options: SELECT_OPTIONS_ENUM.CODE | SELECT_OPTIONS_ENUM.VIEW;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DialogOptions {
|
|
||||||
/**
|
|
||||||
* 点击遮罩层是否可以关闭
|
|
||||||
*/
|
|
||||||
closeOnClickModal?: boolean;
|
|
||||||
/**
|
|
||||||
* 是否可以通过按下 ESC 键关闭 Dialog
|
|
||||||
*/
|
|
||||||
closeOnPressEscape?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DrawerOptions extends DialogOptions {
|
|
||||||
/**
|
|
||||||
* 抽屉的方向
|
|
||||||
*/
|
|
||||||
direction?: 'ltr' | 'rtl' | 'ttb' | 'btt';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ElxRunCodeProps {
|
|
||||||
/**
|
|
||||||
* 代码块内容(高亮后的代码块内容)
|
|
||||||
*/
|
|
||||||
code: string[];
|
|
||||||
/**
|
|
||||||
* 代码块内容(原文)
|
|
||||||
*/
|
|
||||||
content: string;
|
|
||||||
/**
|
|
||||||
* 高亮后pre标签的类名
|
|
||||||
*/
|
|
||||||
preClass: string;
|
|
||||||
/**
|
|
||||||
* 高亮后pre标签的样式
|
|
||||||
*/
|
|
||||||
preStyle: any;
|
|
||||||
/**
|
|
||||||
* 语言
|
|
||||||
*/
|
|
||||||
lang: string;
|
|
||||||
/**
|
|
||||||
* 是否可见
|
|
||||||
*/
|
|
||||||
visible: boolean;
|
|
||||||
/**
|
|
||||||
* 自定义类名
|
|
||||||
*/
|
|
||||||
customClass?: string;
|
|
||||||
/**
|
|
||||||
* 弹窗模式
|
|
||||||
*/
|
|
||||||
mode?: 'dialog' | 'drawer';
|
|
||||||
/**
|
|
||||||
* 弹窗主题(暂时不支持)
|
|
||||||
*/
|
|
||||||
theme?: string;
|
|
||||||
/**
|
|
||||||
* 弹窗选项
|
|
||||||
*/
|
|
||||||
dialogOptions?: DialogOptions;
|
|
||||||
/**
|
|
||||||
* 抽屉选项
|
|
||||||
*/
|
|
||||||
drawerOptions?: DrawerOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ElxRunCodeOptions = Pick<
|
|
||||||
ElxRunCodeProps,
|
|
||||||
'mode' | 'customClass' | 'dialogOptions' | 'drawerOptions'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type OmitOfElxRunCodeContent = Omit<
|
|
||||||
ElxRunCodeProps,
|
|
||||||
'visible' | 'customClass' | 'dialogOptions' | 'drawerOptions'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface ElxRunCodeContentProps extends OmitOfElxRunCodeContent {
|
|
||||||
/**
|
|
||||||
* 当前内容区域显示的视图
|
|
||||||
*/
|
|
||||||
nowView: ElxRunCodeHeaderTypes['options'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ElxRunCodeExposeProps extends ElxRunCodeProps {
|
|
||||||
/**
|
|
||||||
* 当前选中的视图
|
|
||||||
*/
|
|
||||||
value: ElxRunCodeHeaderTypes['options'];
|
|
||||||
/**
|
|
||||||
* 切换视图
|
|
||||||
*/
|
|
||||||
changeSelectValue: (value: ElxRunCodeHeaderTypes['options']) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ElxRunCodeContentExposeProps extends ElxRunCodeContentProps {
|
|
||||||
/**
|
|
||||||
* 当前选中的视图
|
|
||||||
*/
|
|
||||||
value: ElxRunCodeHeaderTypes['options'];
|
|
||||||
/**
|
|
||||||
* 当前内容区域显示的视图
|
|
||||||
*/
|
|
||||||
nowView: ElxRunCodeHeaderTypes['options'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ElxRunCodeCloseBtnExposeProps {
|
|
||||||
close: () => void;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import CodeBlock from './CodeBlock/index.vue';
|
|
||||||
import CodeLine from './CodeLine/index.vue';
|
|
||||||
import CodeX from './CodeX/index.vue';
|
|
||||||
import Mermaid from './Mermaid/index.vue';
|
|
||||||
|
|
||||||
export { CodeBlock, CodeLine, CodeX, Mermaid };
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// import type { CustomAttrs } from './core'
|
|
||||||
|
|
||||||
// export type * from './core/types'
|
|
||||||
import type { BuiltinTheme } from 'shiki';
|
|
||||||
import type { Component } from 'vue';
|
|
||||||
|
|
||||||
export interface MdComponent {
|
|
||||||
raw: any;
|
|
||||||
}
|
|
||||||
export type codeXRenderer =
|
|
||||||
| ((params: { language?: string; content: string }) => VNodeChild)
|
|
||||||
| Component;
|
|
||||||
export type codeXSlot = ((params: any) => VNodeChild) | Component;
|
|
||||||
export interface HighlightProps {
|
|
||||||
theme?: BuiltinTheme | null;
|
|
||||||
isDark?: boolean;
|
|
||||||
language?: string;
|
|
||||||
content?: string;
|
|
||||||
}
|
|
||||||
// 定义颜色替换的类型
|
|
||||||
export interface ColorReplacements {
|
|
||||||
[theme: string]: Record<string, string>;
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import type { Root } from 'hast';
|
|
||||||
import type { Options as TRehypeOptions } from 'mdast-util-to-hast';
|
|
||||||
import type { PluggableList } from 'unified';
|
|
||||||
import type { PropType } from 'vue';
|
|
||||||
|
|
||||||
import type { CustomAttrs, SanitizeOptions, TVueMarkdown } from './types';
|
|
||||||
import { defineComponent, shallowRef, toRefs, watch, onUnmounted } from 'vue';
|
|
||||||
// import { useMarkdownContext } from '../components/MarkdownProvider';
|
|
||||||
import { render } from './hast-to-vnode';
|
|
||||||
import { useMarkdownProcessor } from './useProcessor';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
|
|
||||||
export type { CustomAttrs, SanitizeOptions, TVueMarkdown };
|
|
||||||
|
|
||||||
const sharedProps = {
|
|
||||||
markdown: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
customAttrs: {
|
|
||||||
type: Object as PropType<CustomAttrs>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
remarkPlugins: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypePlugins: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypeOptions: {
|
|
||||||
type: Object as PropType<Omit<TRehypeOptions, 'file'>>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
sanitize: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
sanitizeOptions: {
|
|
||||||
type: Object as PropType<SanitizeOptions>,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const vueMarkdownImpl = defineComponent({
|
|
||||||
name: 'VueMarkdown',
|
|
||||||
props: sharedProps,
|
|
||||||
setup(props, { slots, attrs }) {
|
|
||||||
const {
|
|
||||||
markdown,
|
|
||||||
remarkPlugins,
|
|
||||||
rehypePlugins,
|
|
||||||
rehypeOptions,
|
|
||||||
sanitize,
|
|
||||||
sanitizeOptions,
|
|
||||||
customAttrs
|
|
||||||
} = toRefs(props);
|
|
||||||
|
|
||||||
const { processor } = useMarkdownProcessor({
|
|
||||||
remarkPlugins,
|
|
||||||
rehypePlugins,
|
|
||||||
rehypeOptions,
|
|
||||||
sanitize,
|
|
||||||
sanitizeOptions
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const mdast = processor.value.parse(markdown.value);
|
|
||||||
const hast = processor.value.runSync(mdast) as Root;
|
|
||||||
return render(hast, attrs, slots, customAttrs.value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const vueMarkdownAsyncImpl = defineComponent({
|
|
||||||
name: 'VueMarkdownAsync',
|
|
||||||
props: sharedProps,
|
|
||||||
async setup(props, { slots, attrs }) {
|
|
||||||
const {
|
|
||||||
markdown,
|
|
||||||
remarkPlugins,
|
|
||||||
rehypePlugins,
|
|
||||||
rehypeOptions,
|
|
||||||
sanitize,
|
|
||||||
sanitizeOptions,
|
|
||||||
customAttrs
|
|
||||||
} = toRefs(props);
|
|
||||||
const { processor } = useMarkdownProcessor({
|
|
||||||
remarkPlugins,
|
|
||||||
rehypePlugins,
|
|
||||||
rehypeOptions,
|
|
||||||
sanitize,
|
|
||||||
sanitizeOptions
|
|
||||||
});
|
|
||||||
|
|
||||||
const hast = shallowRef<Root | null>(null);
|
|
||||||
const isProcessing = shallowRef(false);
|
|
||||||
|
|
||||||
const process = async (): Promise<void> => {
|
|
||||||
// 避免重复处理
|
|
||||||
if (isProcessing.value) return;
|
|
||||||
isProcessing.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const mdast = processor.value.parse(markdown.value);
|
|
||||||
hast.value = (await processor.value.run(mdast)) as Root;
|
|
||||||
} finally {
|
|
||||||
isProcessing.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用防抖优化SSE流式更新场景
|
|
||||||
// trailing: true 确保最后一次更新会执行
|
|
||||||
// leading: false 避免首次触发两次渲染
|
|
||||||
const debouncedProcess = debounce(process, 50, {
|
|
||||||
leading: false,
|
|
||||||
trailing: true,
|
|
||||||
maxWait: 200 // 最长等待200ms,避免延迟过大
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [markdown.value, processor.value],
|
|
||||||
() => {
|
|
||||||
debouncedProcess();
|
|
||||||
},
|
|
||||||
{ flush: 'post' } // 改为post,在DOM更新后执行,避免阻塞UI
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清理防抖定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
debouncedProcess.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
await process();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return hast.value
|
|
||||||
? render(hast.value, attrs, slots, customAttrs.value)
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// export the public type for h/tsx inference
|
|
||||||
// also to avoid inline import() in generated d.ts files
|
|
||||||
export const VueMarkdown: TVueMarkdown = vueMarkdownImpl as any;
|
|
||||||
|
|
||||||
// export the public type for h/tsx inference
|
|
||||||
// also to avoid inline import() in generated d.ts files
|
|
||||||
export const VueMarkdownAsync: TVueMarkdown = vueMarkdownAsyncImpl as any;
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import type { Element, Root, RootContent, Text } from 'hast';
|
|
||||||
import type { MaybeRefOrGetter, Slots, VNode, VNodeArrayChildren } from 'vue';
|
|
||||||
import type {
|
|
||||||
AliasList,
|
|
||||||
Attributes,
|
|
||||||
Context,
|
|
||||||
CustomAttrs,
|
|
||||||
CustomAttrsObjectResult
|
|
||||||
} from './types';
|
|
||||||
import { find, html, svg } from 'property-information';
|
|
||||||
import { h, toValue } from 'vue';
|
|
||||||
|
|
||||||
export function render(
|
|
||||||
hast: Root,
|
|
||||||
attrs: Record<string, unknown>,
|
|
||||||
slots?: Slots,
|
|
||||||
customAttrs?: MaybeRefOrGetter<CustomAttrs>
|
|
||||||
): VNode {
|
|
||||||
return h(
|
|
||||||
'div',
|
|
||||||
attrs,
|
|
||||||
renderChildren(
|
|
||||||
hast.children,
|
|
||||||
{ listDepth: -1, listOrdered: false, listItemIndex: -1, svg: false },
|
|
||||||
hast,
|
|
||||||
slots ?? {},
|
|
||||||
toValue(customAttrs) ?? {}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderChildren(
|
|
||||||
nodeList: (RootContent | Root)[],
|
|
||||||
ctx: Context,
|
|
||||||
parent: Element | Root,
|
|
||||||
slots: Slots,
|
|
||||||
customAttrs: CustomAttrs
|
|
||||||
): VNodeArrayChildren {
|
|
||||||
const keyCounter: {
|
|
||||||
[key: string]: number;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
return nodeList.map(node => {
|
|
||||||
switch (node.type) {
|
|
||||||
case 'text':
|
|
||||||
return node.value;
|
|
||||||
case 'raw':
|
|
||||||
return node.value;
|
|
||||||
case 'root':
|
|
||||||
return renderChildren(node.children, ctx, parent, slots, customAttrs);
|
|
||||||
case 'element': {
|
|
||||||
const { attrs, context, aliasList, vnodeProps } = getVNodeInfos(
|
|
||||||
node,
|
|
||||||
parent,
|
|
||||||
ctx,
|
|
||||||
keyCounter,
|
|
||||||
customAttrs
|
|
||||||
);
|
|
||||||
for (let i = aliasList.length - 1; i >= 0; i--) {
|
|
||||||
const targetSlot = slots[aliasList[i]];
|
|
||||||
if (typeof targetSlot === 'function') {
|
|
||||||
return targetSlot({
|
|
||||||
...vnodeProps,
|
|
||||||
...attrs,
|
|
||||||
children: () =>
|
|
||||||
renderChildren(node.children, context, node, slots, customAttrs)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(
|
|
||||||
node.tagName,
|
|
||||||
attrs,
|
|
||||||
renderChildren(node.children, context, node, slots, customAttrs)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVNodeInfos(
|
|
||||||
node: RootContent,
|
|
||||||
parent: Element | Root,
|
|
||||||
context: Context,
|
|
||||||
keyCounter: Record<string, number>,
|
|
||||||
customAttrs: CustomAttrs
|
|
||||||
): {
|
|
||||||
attrs: Record<string, unknown>;
|
|
||||||
context: Context;
|
|
||||||
aliasList: AliasList;
|
|
||||||
vnodeProps: Record<string, any>;
|
|
||||||
} {
|
|
||||||
const aliasList: AliasList = [];
|
|
||||||
|
|
||||||
let attrs: Record<string, unknown> = {};
|
|
||||||
const vnodeProps: Record<string, any> = {};
|
|
||||||
const ctx = { ...context };
|
|
||||||
|
|
||||||
if (node.type === 'element') {
|
|
||||||
aliasList.push(node.tagName);
|
|
||||||
keyCounter[node.tagName] =
|
|
||||||
node.tagName in keyCounter ? keyCounter[node.tagName] + 1 : 0;
|
|
||||||
vnodeProps.key = `${node.tagName}-${keyCounter[node.tagName]}`;
|
|
||||||
node.properties = node.properties || {};
|
|
||||||
|
|
||||||
if (node.tagName === 'svg') {
|
|
||||||
ctx.svg = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs = Object.entries(node.properties).reduce<Record<string, any>>(
|
|
||||||
(acc, [hastKey, value]) => {
|
|
||||||
const attrInfo = find(ctx.svg ? svg : html, hastKey);
|
|
||||||
acc[attrInfo.attribute] = value;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (node.tagName) {
|
|
||||||
case 'h1':
|
|
||||||
case 'h2':
|
|
||||||
case 'h3':
|
|
||||||
case 'h4':
|
|
||||||
case 'h5':
|
|
||||||
case 'h6':
|
|
||||||
vnodeProps.level = Number.parseFloat(node.tagName.slice(1));
|
|
||||||
aliasList.push('heading');
|
|
||||||
break;
|
|
||||||
// TODO: maybe use <pre> instead for customizing from <pre> not <code> ?
|
|
||||||
case 'code':
|
|
||||||
vnodeProps.languageOriginal = Array.isArray(attrs.class)
|
|
||||||
? attrs.class.find(cls => cls.startsWith('language-'))
|
|
||||||
: '';
|
|
||||||
vnodeProps.language = vnodeProps.languageOriginal
|
|
||||||
? vnodeProps.languageOriginal.replace('language-', '')
|
|
||||||
: '';
|
|
||||||
vnodeProps.inline = 'tagName' in parent && parent.tagName !== 'pre';
|
|
||||||
|
|
||||||
// when tagName is code, it definitely has children and the first child is text
|
|
||||||
// https://github.com/syntax-tree/mdast-util-to-hast/blob/main/lib/handlers/code.js
|
|
||||||
vnodeProps.content = (node.children[0] as unknown as Text)?.value ?? '';
|
|
||||||
|
|
||||||
aliasList.push(vnodeProps.inline ? 'inline-code' : 'block-code');
|
|
||||||
break;
|
|
||||||
case 'thead':
|
|
||||||
case 'tbody':
|
|
||||||
ctx.currentContext = node.tagName;
|
|
||||||
break;
|
|
||||||
case 'td':
|
|
||||||
case 'th':
|
|
||||||
case 'tr':
|
|
||||||
vnodeProps.isHead = context.currentContext === 'thead';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ul':
|
|
||||||
case 'ol':
|
|
||||||
ctx.listDepth = context.listDepth + 1;
|
|
||||||
ctx.listOrdered = node.tagName === 'ol';
|
|
||||||
ctx.listItemIndex = -1;
|
|
||||||
vnodeProps.ordered = ctx.listOrdered;
|
|
||||||
vnodeProps.depth = ctx.listDepth;
|
|
||||||
|
|
||||||
aliasList.push('list');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'li':
|
|
||||||
ctx.listItemIndex++;
|
|
||||||
|
|
||||||
vnodeProps.ordered = ctx.listOrdered;
|
|
||||||
vnodeProps.depth = ctx.listDepth;
|
|
||||||
vnodeProps.index = ctx.listItemIndex;
|
|
||||||
aliasList.push('list-item');
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'slot':
|
|
||||||
if (typeof node.properties['slot-name'] === 'string') {
|
|
||||||
aliasList.push(`${node.properties['slot-name']}`);
|
|
||||||
delete node.properties['slot-name'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs = computeAttrs(
|
|
||||||
node,
|
|
||||||
aliasList,
|
|
||||||
vnodeProps,
|
|
||||||
{ ...attrs } as Attributes, // TODO: fix this
|
|
||||||
customAttrs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
attrs,
|
|
||||||
context: ctx,
|
|
||||||
aliasList,
|
|
||||||
vnodeProps
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO:
|
|
||||||
* @param node - hast node
|
|
||||||
* @param aliasList - html tag list. The earlier alias has higher priority. ?
|
|
||||||
* @param attrs - attrs
|
|
||||||
* @param customAttrs - custom attrs object
|
|
||||||
* @returns attrs
|
|
||||||
*/
|
|
||||||
function computeAttrs(
|
|
||||||
node: Element,
|
|
||||||
aliasList: AliasList,
|
|
||||||
vnodeProps: Record<string, any>,
|
|
||||||
attrs: Attributes,
|
|
||||||
customAttrs: CustomAttrs
|
|
||||||
): CustomAttrsObjectResult {
|
|
||||||
const result: CustomAttrsObjectResult = {
|
|
||||||
...attrs
|
|
||||||
};
|
|
||||||
for (let i = aliasList.length - 1; i >= 0; i--) {
|
|
||||||
const name = aliasList[i];
|
|
||||||
// console.log(Object.keys(customAttrs))
|
|
||||||
if (name in customAttrs) {
|
|
||||||
const customAttr = customAttrs[name];
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
...(typeof customAttr === 'function'
|
|
||||||
? customAttr(node, { ...attrs, ...vnodeProps })
|
|
||||||
: customAttr)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// shunnNet has the rights under the MIT license
|
|
||||||
export { VueMarkdown, VueMarkdownAsync } from './components';
|
|
||||||
export { getVNodeInfos, render, renderChildren } from './hast-to-vnode';
|
|
||||||
export type * from './types';
|
|
||||||
export { createProcessor, useMarkdownProcessor } from './useProcessor';
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
import type { Options as DeepMergeOptions } from 'deepmerge';
|
|
||||||
import type { Element } from 'hast';
|
|
||||||
import type { Options as TRehypeOptions } from 'mdast-util-to-hast';
|
|
||||||
import type { Options } from 'rehype-sanitize';
|
|
||||||
import type { PluggableList } from 'unified';
|
|
||||||
import type {
|
|
||||||
AllowedComponentProps,
|
|
||||||
ComponentCustomProps,
|
|
||||||
VNode,
|
|
||||||
VNodeArrayChildren,
|
|
||||||
VNodeProps
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
export interface Context {
|
|
||||||
listDepth: number;
|
|
||||||
listOrdered: boolean;
|
|
||||||
listItemIndex: number;
|
|
||||||
currentContext?: string;
|
|
||||||
svg: boolean;
|
|
||||||
}
|
|
||||||
export type Attributes = Record<string, string>;
|
|
||||||
|
|
||||||
interface TTableProps {
|
|
||||||
/** whether it is in head */
|
|
||||||
isHead: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface THeadingProps {
|
|
||||||
/** heading level */
|
|
||||||
level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TListProps {
|
|
||||||
/** depth of the list */
|
|
||||||
depth: number;
|
|
||||||
/** whether it is ordered list */
|
|
||||||
ordered: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TCodeProps {
|
|
||||||
/** language name original @example 'language-js' */
|
|
||||||
languageOriginal: string;
|
|
||||||
|
|
||||||
/** language name @example 'js' */
|
|
||||||
language: string;
|
|
||||||
|
|
||||||
/** code content */
|
|
||||||
content: string;
|
|
||||||
|
|
||||||
/** whether it is inline code */
|
|
||||||
inline: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://www.google.com/search?q=record%3Cstring,+any%3E+vs+record%3Cstring,+unknown%3E&sourceid=chrome&ie=UTF-8
|
|
||||||
export type CustomAttrsObjectResult = Record<string, unknown>;
|
|
||||||
|
|
||||||
type CustomAttrsFunctionValue<T> = (
|
|
||||||
/**
|
|
||||||
* hast node
|
|
||||||
*
|
|
||||||
* Please refer to the source code at the following URL to understand the possible attributes for each element.
|
|
||||||
*
|
|
||||||
* @see https://github.com/syntax-tree/mdast-util-to-hast/tree/main/lib/handlers
|
|
||||||
*/
|
|
||||||
node: Element,
|
|
||||||
/**
|
|
||||||
* Properties of the current element.
|
|
||||||
*
|
|
||||||
* Except for the basic properties provided from hast, it also includes custom properties such as `level`, `ordered`, `depth`, `index` etc.
|
|
||||||
*/
|
|
||||||
combinedAttrs: T | Attributes
|
|
||||||
) => Record<string, unknown>;
|
|
||||||
|
|
||||||
type CustomAttrsValue<
|
|
||||||
T extends Record<string, unknown> = Record<string, unknown>
|
|
||||||
> = CustomAttrsObjectResult | CustomAttrsFunctionValue<T>;
|
|
||||||
|
|
||||||
type TBasicHTMLTagNames = keyof Omit<
|
|
||||||
HTMLElementTagNameMap,
|
|
||||||
| 'h1'
|
|
||||||
| 'h2'
|
|
||||||
| 'h3'
|
|
||||||
| 'h4'
|
|
||||||
| 'h5'
|
|
||||||
| 'h6'
|
|
||||||
| 'ul'
|
|
||||||
| 'ol'
|
|
||||||
| 'li'
|
|
||||||
| 'code'
|
|
||||||
| 'td'
|
|
||||||
| 'th'
|
|
||||||
| 'tr'
|
|
||||||
>;
|
|
||||||
export type CustomAttrs = {
|
|
||||||
[key in TBasicHTMLTagNames]?: CustomAttrsValue; // << dynamic properties
|
|
||||||
} & {
|
|
||||||
[key: string]:
|
|
||||||
| CustomAttrsValue
|
|
||||||
| CustomAttrsValue<THeadingProps>
|
|
||||||
| CustomAttrsValue<TListProps>
|
|
||||||
| CustomAttrsValue<TCodeProps>
|
|
||||||
| CustomAttrsValue<TTableProps>
|
|
||||||
| undefined;
|
|
||||||
['h1']?: CustomAttrsValue<THeadingProps>; // << static properties
|
|
||||||
['h2']?: CustomAttrsValue<THeadingProps>;
|
|
||||||
['h3']?: CustomAttrsValue<THeadingProps>;
|
|
||||||
['h4']?: CustomAttrsValue<THeadingProps>;
|
|
||||||
['h5']?: CustomAttrsValue<THeadingProps>;
|
|
||||||
['h6']?: CustomAttrsValue<THeadingProps>;
|
|
||||||
['heading']?: CustomAttrsValue<THeadingProps>;
|
|
||||||
['ul']?: CustomAttrsValue<TListProps>;
|
|
||||||
['ol']?: CustomAttrsValue<TListProps>;
|
|
||||||
['list']?: CustomAttrsValue<TListProps>;
|
|
||||||
['li']?: CustomAttrsValue<TListProps>;
|
|
||||||
['list-item']?: CustomAttrsValue<TListProps>;
|
|
||||||
['code']?: CustomAttrsValue<TCodeProps>;
|
|
||||||
['inline-code']?: CustomAttrsValue<TCodeProps>;
|
|
||||||
['block-code']?: CustomAttrsValue<TCodeProps>;
|
|
||||||
['td']?: CustomAttrsValue<TTableProps>;
|
|
||||||
['th']?: CustomAttrsValue<TTableProps>;
|
|
||||||
['tr']?: CustomAttrsValue<TTableProps>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AliasList = string[];
|
|
||||||
export type TagList = AliasList;
|
|
||||||
|
|
||||||
export interface SanitizeOptions {
|
|
||||||
/**
|
|
||||||
* Options for `rehype-sanitize`
|
|
||||||
*
|
|
||||||
* @see https://github.com/rehypejs/rehype-sanitize
|
|
||||||
*/
|
|
||||||
sanitizeOptions?: Options;
|
|
||||||
/**
|
|
||||||
* Options for `deepmerge`
|
|
||||||
*/
|
|
||||||
mergeOptions?: DeepMergeOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TVueMarkdownProps {
|
|
||||||
/**
|
|
||||||
* Markdown content
|
|
||||||
*
|
|
||||||
* @default '''
|
|
||||||
*/
|
|
||||||
markdown: string;
|
|
||||||
/**
|
|
||||||
* You can set custom attributes for each element, such as `href`, `target`, `rel`, `lazyload`, etc.
|
|
||||||
*
|
|
||||||
* The key is the HTML tag name, and the value can either be an object or a function that returns an object.
|
|
||||||
*
|
|
||||||
* The value will be passed to Vue's `h` function. You can refer to Vue's official documentation to learn how to configure `h`.
|
|
||||||
*
|
|
||||||
* @see https://vuejs.org/guide/extras/render-function.html#render-functions-jsx
|
|
||||||
*
|
|
||||||
* @default {}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* a: { target: '_blank', rel: 'noopener' },
|
|
||||||
* img: { lazyload: true },
|
|
||||||
* h1: (node, combinedAttrs) => {
|
|
||||||
* return { class: ['title', 'mb-2'] }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
customAttrs?: CustomAttrs;
|
|
||||||
/**
|
|
||||||
* Remark plugins
|
|
||||||
*
|
|
||||||
* These plugins will be used between `remark-parse` and `remark-rehype`.
|
|
||||||
*
|
|
||||||
* @see https://github.com/remarkjs/remark?tab=readme-ov-file#plugins
|
|
||||||
*
|
|
||||||
* @default []
|
|
||||||
*/
|
|
||||||
remarkPlugins?: PluggableList;
|
|
||||||
/**
|
|
||||||
* rehype plugins
|
|
||||||
*
|
|
||||||
* These plugins will be used after `remark-rehype` but before `rehype-sanitize`.
|
|
||||||
*
|
|
||||||
* @see https://github.com/remarkjs/remark-rehype?tab=readme-ov-file#related
|
|
||||||
*
|
|
||||||
* @default []
|
|
||||||
*/
|
|
||||||
rehypePlugins?: PluggableList;
|
|
||||||
/**
|
|
||||||
* Whether to sanitize the HTML content. (use `rehype-sanitize`)
|
|
||||||
*
|
|
||||||
* You need disable this option if you want to render `<slot>` in markdown content.
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
sanitize?: boolean;
|
|
||||||
/**
|
|
||||||
* Options for `rehype-sanitize`
|
|
||||||
*
|
|
||||||
* @see https://github.com/rehypejs/rehype-sanitize?tab=readme-ov-file#options
|
|
||||||
*
|
|
||||||
* @default { allowDangerousHtml: true }
|
|
||||||
*/
|
|
||||||
sanitizeOptions?: SanitizeOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for `rehype-parse`
|
|
||||||
*
|
|
||||||
* @see https://github.com/remarkjs/remark-rehype?tab=readme-ov-file#options
|
|
||||||
*
|
|
||||||
* @default {}
|
|
||||||
*/
|
|
||||||
rehypeOptions?: Omit<TRehypeOptions, 'file'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Typed version of the `VueMarkdown` component.
|
|
||||||
*
|
|
||||||
* Copy from vue-router
|
|
||||||
*/
|
|
||||||
export interface TVueMarkdown {
|
|
||||||
new (): {
|
|
||||||
$props: AllowedComponentProps &
|
|
||||||
ComponentCustomProps &
|
|
||||||
VNodeProps &
|
|
||||||
TVueMarkdownProps;
|
|
||||||
|
|
||||||
$slots: TBaseSlots & {
|
|
||||||
/**
|
|
||||||
* Customize `<h1>` tag
|
|
||||||
* @scope `level` heading level
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['h1']?: THeadingSlot;
|
|
||||||
/**
|
|
||||||
* Customize `<h2>` tag
|
|
||||||
* @scope `level` heading level
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['h2']?: THeadingSlot;
|
|
||||||
/**
|
|
||||||
* Customize `<h3>` tag
|
|
||||||
* @scope `level` heading level
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['h3']?: THeadingSlot;
|
|
||||||
/**
|
|
||||||
* Customize `<h4>` tag
|
|
||||||
* @scope `level` heading level
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['h4']?: THeadingSlot;
|
|
||||||
/**
|
|
||||||
* Customize `<h5>` tag
|
|
||||||
* @scope `level` heading level
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['h5']?: THeadingSlot;
|
|
||||||
/**
|
|
||||||
* Customize `<h6>` tag
|
|
||||||
* @scope `level` heading level
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['h6']?: THeadingSlot;
|
|
||||||
/**
|
|
||||||
* Customize `<h1>` ~ `<h6>` tag
|
|
||||||
* @scope `level` heading level
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['heading']?: THeadingSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize inline and block code.
|
|
||||||
* @scope `languageOriginal` language name original
|
|
||||||
* @scope `language` language name
|
|
||||||
* @scope `content` code content
|
|
||||||
* @scope `inline` whether it is inline code
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['code']?: TCodeSlot;
|
|
||||||
/**
|
|
||||||
* Customize inline code.
|
|
||||||
* @scope `languageOriginal` language name original
|
|
||||||
* @scope `language` language name
|
|
||||||
* @scope `content` code content
|
|
||||||
* @scope `inline` whether it is inline code
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['inline-code']?: TCodeSlot;
|
|
||||||
/**
|
|
||||||
* Customize block code.
|
|
||||||
* @scope `languageOriginal` language name original
|
|
||||||
* @scope `language` language name
|
|
||||||
* @scope `content` code content
|
|
||||||
* @scope `inline` whether it is inline code
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['block-code']?: TCodeSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize unordered list
|
|
||||||
* @scope `depth` depth of the list
|
|
||||||
* @scope `ordered` whether it is ordered list
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['ul']?: TListSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize ordered list
|
|
||||||
* @scope `depth` depth of the list
|
|
||||||
* @scope `ordered` whether it is ordered list
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['ol']?: TListSlot;
|
|
||||||
/**
|
|
||||||
* Customize ordered and unordered list
|
|
||||||
* @scope `depth` depth of the list
|
|
||||||
* @scope `ordered` whether it is ordered list
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['list']?: TListSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize list item
|
|
||||||
* @scope `depth` depth of the list
|
|
||||||
* @scope `ordered` whether it is ordered list
|
|
||||||
* @scope `index` index of the list item
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['li']?: TListSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize list item
|
|
||||||
* @scope `depth` depth of the list
|
|
||||||
* @scope `ordered` whether it is ordered list
|
|
||||||
* @scope `index` index of the list item
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['list-item']?: TListSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize table element: td
|
|
||||||
*
|
|
||||||
* @scope `isHead` whether it is in head
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['td']?: TTableElementSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize table element: th
|
|
||||||
*
|
|
||||||
* @scope `isHead` whether it is in head
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['th']?: TTableElementSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customize table element: tr
|
|
||||||
*
|
|
||||||
* @scope `isHead` whether it is in head
|
|
||||||
* @scope `children` Functional component, child elements.
|
|
||||||
*/
|
|
||||||
['tr']?: TTableElementSlot;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type TTableElementSlot = TCustomSlot<TTableProps>;
|
|
||||||
type TListSlot = TCustomSlot<TListProps>;
|
|
||||||
type THeadingSlot = TCustomSlot<THeadingProps>;
|
|
||||||
type TCodeSlot = TCustomSlot<TCodeProps>;
|
|
||||||
|
|
||||||
type HtmlTagNames = keyof HTMLElementTagNameMap;
|
|
||||||
type TBaseSlots = {
|
|
||||||
// An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.ts(1337)
|
|
||||||
// [key: HtmlTagNames]: (scope: Record<string, any>) => VNode[] | VNode
|
|
||||||
[key in HtmlTagNames]?: (scope: TBaseSlotScope) => VNode[] | VNode;
|
|
||||||
} & {
|
|
||||||
[key: string]: (scope: TBaseSlotScope) => VNode[] | VNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TBaseSlotScope = TElementChild & Attributes;
|
|
||||||
interface TElementChild {
|
|
||||||
/** Functional component, child elements. */
|
|
||||||
children: () => VNodeArrayChildren;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCustomSlot<T> = (scope: TBaseSlotScope & T) => VNode[] | VNode;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import type { Root } from 'hast';
|
|
||||||
import type { Root as MdastRoot } from 'mdast';
|
|
||||||
import type { Options as TRehypeOptions } from 'mdast-util-to-hast';
|
|
||||||
import type { PluggableList, Processor } from 'unified';
|
|
||||||
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
|
|
||||||
import type { SanitizeOptions } from './types';
|
|
||||||
import deepmerge from 'deepmerge';
|
|
||||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
|
||||||
import remarkParse from 'remark-parse';
|
|
||||||
import remarkRehype from 'remark-rehype';
|
|
||||||
import { unified } from 'unified';
|
|
||||||
import { computed, toValue } from 'vue';
|
|
||||||
|
|
||||||
export interface TUseMarkdownProcessorOptions {
|
|
||||||
remarkPlugins?: MaybeRefOrGetter<PluggableList>;
|
|
||||||
rehypePlugins?: MaybeRefOrGetter<PluggableList>;
|
|
||||||
rehypeOptions?: MaybeRefOrGetter<Omit<TRehypeOptions, 'file'>>;
|
|
||||||
sanitize?: MaybeRefOrGetter<boolean>;
|
|
||||||
sanitizeOptions?: MaybeRefOrGetter<SanitizeOptions>;
|
|
||||||
}
|
|
||||||
export function useMarkdownProcessor(options?: TUseMarkdownProcessorOptions): {
|
|
||||||
processor: ComputedRef<
|
|
||||||
Processor<MdastRoot, MdastRoot, Root, undefined, undefined>
|
|
||||||
>;
|
|
||||||
} {
|
|
||||||
const processor = computed(() => {
|
|
||||||
return createProcessor({
|
|
||||||
prePlugins: [remarkParse, ...(toValue(options?.remarkPlugins) ?? [])],
|
|
||||||
rehypePlugins: toValue(options?.rehypePlugins),
|
|
||||||
rehypeOptions: toValue(options?.rehypeOptions),
|
|
||||||
sanitize: toValue(options?.sanitize),
|
|
||||||
sanitizeOptions: toValue(options?.sanitizeOptions)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return { processor };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProcessor(options?: {
|
|
||||||
prePlugins?: PluggableList;
|
|
||||||
rehypePlugins?: PluggableList;
|
|
||||||
rehypeOptions?: Omit<TRehypeOptions, 'file'>;
|
|
||||||
sanitize?: boolean;
|
|
||||||
sanitizeOptions?: SanitizeOptions;
|
|
||||||
// TODO: fix types
|
|
||||||
}): Processor<any, any, any, any, any> {
|
|
||||||
return unified()
|
|
||||||
.use(options?.prePlugins ?? [])
|
|
||||||
.use(remarkRehype, {
|
|
||||||
allowDangerousHtml: true,
|
|
||||||
...(options?.rehypeOptions || {})
|
|
||||||
})
|
|
||||||
.use(options?.rehypePlugins ?? [])
|
|
||||||
.use(
|
|
||||||
options?.sanitize
|
|
||||||
? [
|
|
||||||
[
|
|
||||||
rehypeSanitize,
|
|
||||||
deepmerge(
|
|
||||||
defaultSchema,
|
|
||||||
options?.sanitizeOptions?.sanitizeOptions || {},
|
|
||||||
options?.sanitizeOptions?.mergeOptions || {}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export * from './useComponents';
|
|
||||||
export * from './useMarkdown';
|
|
||||||
export * from './useMermaid';
|
|
||||||
export * from './useMermaidZoom';
|
|
||||||
export * from './usePlugins';
|
|
||||||
export * from './useThemeMode';
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { h } from 'vue';
|
|
||||||
import { CodeX } from '../components/index';
|
|
||||||
|
|
||||||
function useComponents() {
|
|
||||||
const components = {
|
|
||||||
code: (raw: any) => h(CodeX, { raw })
|
|
||||||
};
|
|
||||||
return components;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useComponents };
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { flow } from 'lodash-es';
|
|
||||||
|
|
||||||
export function useProcessMarkdown(markdown: string) {
|
|
||||||
return preprocessLaTeX(markdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preprocessLaTeX(markdown: string) {
|
|
||||||
if (typeof markdown !== 'string') return markdown;
|
|
||||||
|
|
||||||
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
||||||
const codeBlocks = markdown.match(codeBlockRegex) || [];
|
|
||||||
const escapeReplacement = (str: string) => str.replace(/\$/g, '_ELX_DOLLAR_');
|
|
||||||
let processedMarkdown = markdown.replace(
|
|
||||||
codeBlockRegex,
|
|
||||||
'ELX_CODE_BLOCK_PLACEHOLDER'
|
|
||||||
);
|
|
||||||
|
|
||||||
processedMarkdown = flow([
|
|
||||||
(str: string) =>
|
|
||||||
str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
|
|
||||||
(str: string) =>
|
|
||||||
str.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`),
|
|
||||||
(str: string) =>
|
|
||||||
str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
|
|
||||||
(str: string) =>
|
|
||||||
str.replace(
|
|
||||||
/(^|[^\\])\$(.+?)\$/g,
|
|
||||||
(_, prefix, equation) => `${prefix}$${equation}$`
|
|
||||||
)
|
|
||||||
])(processedMarkdown);
|
|
||||||
|
|
||||||
codeBlocks.forEach(block => {
|
|
||||||
processedMarkdown = processedMarkdown.replace(
|
|
||||||
'ELX_CODE_BLOCK_PLACEHOLDER',
|
|
||||||
escapeReplacement(block)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
processedMarkdown = processedMarkdown.replace(/_ELX_DOLLAR_/g, '$');
|
|
||||||
|
|
||||||
return processedMarkdown;
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import type { Ref } from 'vue';
|
|
||||||
import { throttle } from 'lodash-es';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
interface UseMermaidOptions {
|
|
||||||
id?: string;
|
|
||||||
theme?: 'default' | 'dark' | 'forest' | 'neutral' | string;
|
|
||||||
config?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMermaid() {
|
|
||||||
if (typeof window === 'undefined') return null;
|
|
||||||
const mermaidModule = await import('mermaid');
|
|
||||||
return mermaidModule.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mermaidContainer: HTMLElement | null = null;
|
|
||||||
|
|
||||||
function getMermaidContainer(): HTMLElement {
|
|
||||||
if (!mermaidContainer) {
|
|
||||||
mermaidContainer = document.querySelector(
|
|
||||||
'.elx-markdown-mermaid-container'
|
|
||||||
) as HTMLElement;
|
|
||||||
if (!mermaidContainer) {
|
|
||||||
mermaidContainer = document.createElement('div') as HTMLElement;
|
|
||||||
mermaidContainer.ariaHidden = 'true';
|
|
||||||
mermaidContainer.style.maxHeight = '0';
|
|
||||||
mermaidContainer.style.opacity = '0';
|
|
||||||
mermaidContainer.style.overflow = 'hidden';
|
|
||||||
mermaidContainer.classList.add('elx-markdown-mermaid-container');
|
|
||||||
document.body.append(mermaidContainer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mermaidContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMermaid(
|
|
||||||
content: string | Ref<string>,
|
|
||||||
options: UseMermaidOptions = {}
|
|
||||||
) {
|
|
||||||
const { id = 'mermaid', theme = 'default', config = {} } = options;
|
|
||||||
const mermaidConfig = computed(() => ({
|
|
||||||
suppressErrorRendering: true,
|
|
||||||
startOnLoad: false,
|
|
||||||
securityLevel: 'loose',
|
|
||||||
theme,
|
|
||||||
...config
|
|
||||||
}));
|
|
||||||
const data = ref('');
|
|
||||||
const error = ref<unknown>(null);
|
|
||||||
const throttledRender = throttle(
|
|
||||||
async () => {
|
|
||||||
const contentValue =
|
|
||||||
typeof content === 'string' ? content : content.value;
|
|
||||||
if (!contentValue?.trim()) {
|
|
||||||
data.value = '';
|
|
||||||
error.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 动态加载 mermaid 库
|
|
||||||
const mermaidInstance = await loadMermaid();
|
|
||||||
if (!mermaidInstance) {
|
|
||||||
data.value = contentValue;
|
|
||||||
error.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 语法校验
|
|
||||||
const isValid = await mermaidInstance.parse(contentValue.trim());
|
|
||||||
if (!isValid) {
|
|
||||||
console.log('Mermaid parse error: Invalid syntax');
|
|
||||||
data.value = '';
|
|
||||||
error.value = new Error('Mermaid parse error: Invalid syntax');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 初始化 mermaid 配置
|
|
||||||
mermaidInstance.initialize(mermaidConfig.value);
|
|
||||||
const renderId = `${id}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
const container = getMermaidContainer();
|
|
||||||
const { svg } = await mermaidInstance.render(
|
|
||||||
renderId,
|
|
||||||
contentValue,
|
|
||||||
container
|
|
||||||
);
|
|
||||||
data.value = svg;
|
|
||||||
error.value = null;
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Mermaid render error:', err);
|
|
||||||
data.value = '';
|
|
||||||
error.value = err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
300,
|
|
||||||
{ trailing: true, leading: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听内容变化,自动触发渲染
|
|
||||||
watch(
|
|
||||||
() => (typeof content === 'string' ? content : content.value),
|
|
||||||
() => {
|
|
||||||
throttledRender();
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import type {
|
|
||||||
MermaidZoomControls,
|
|
||||||
UseMermaidZoomOptions
|
|
||||||
} from '../components/Mermaid/types';
|
|
||||||
import { onUnmounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
export function useMermaidZoom(
|
|
||||||
options: UseMermaidZoomOptions
|
|
||||||
): MermaidZoomControls {
|
|
||||||
const { container } = options;
|
|
||||||
|
|
||||||
const scale = ref(1);
|
|
||||||
const posX = ref(0);
|
|
||||||
const posY = ref(0);
|
|
||||||
const isDragging = ref(false);
|
|
||||||
|
|
||||||
let removeEvents: (() => void) | null = null;
|
|
||||||
|
|
||||||
// 获取SVG元素
|
|
||||||
const getSvg = () =>
|
|
||||||
container.value?.querySelector('.mermaid-content svg') as HTMLElement;
|
|
||||||
|
|
||||||
// 更新变换
|
|
||||||
const updateTransform = (svg: HTMLElement) => {
|
|
||||||
svg.style.transformOrigin = 'center center';
|
|
||||||
svg.style.transform = `translate(${posX.value}px, ${posY.value}px) scale(${scale.value})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置状态
|
|
||||||
const resetState = () => {
|
|
||||||
scale.value = 1;
|
|
||||||
posX.value = 0;
|
|
||||||
posY.value = 0;
|
|
||||||
isDragging.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加拖拽事件
|
|
||||||
const addDragEvents = (content: HTMLElement) => {
|
|
||||||
let startX = 0;
|
|
||||||
let startY = 0;
|
|
||||||
|
|
||||||
const onStart = (clientX: number, clientY: number) => {
|
|
||||||
isDragging.value = true;
|
|
||||||
startX = clientX - posX.value;
|
|
||||||
startY = clientY - posY.value;
|
|
||||||
document.body.style.userSelect = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMove = (clientX: number, clientY: number) => {
|
|
||||||
if (isDragging.value) {
|
|
||||||
posX.value = clientX - startX;
|
|
||||||
posY.value = clientY - startY;
|
|
||||||
updateTransform(content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEnd = () => {
|
|
||||||
isDragging.value = false;
|
|
||||||
document.body.style.userSelect = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 鼠标事件
|
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
|
||||||
if (e.button !== 0)
|
|
||||||
return; // ⭐️ 只响应鼠标左键
|
|
||||||
e.preventDefault();
|
|
||||||
onStart(e.clientX, e.clientY);
|
|
||||||
};
|
|
||||||
const onMouseMove = (e: MouseEvent) => onMove(e.clientX, e.clientY);
|
|
||||||
|
|
||||||
// 触摸事件
|
|
||||||
const onTouchStart = (e: TouchEvent) => {
|
|
||||||
if (e.touches.length === 1) {
|
|
||||||
onStart(e.touches[0].clientX, e.touches[0].clientY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onTouchMove = (e: TouchEvent) => {
|
|
||||||
if (e.touches.length === 1) {
|
|
||||||
e.preventDefault();
|
|
||||||
onMove(e.touches[0].clientX, e.touches[0].clientY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
content.addEventListener('mousedown', onMouseDown);
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
document.addEventListener('mouseup', onEnd);
|
|
||||||
content.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
||||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
||||||
document.addEventListener('touchend', onEnd);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
content.removeEventListener('mousedown', onMouseDown);
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
|
||||||
document.removeEventListener('mouseup', onEnd);
|
|
||||||
content.removeEventListener('touchstart', onTouchStart);
|
|
||||||
document.removeEventListener('touchmove', onTouchMove);
|
|
||||||
document.removeEventListener('touchend', onEnd);
|
|
||||||
document.body.style.userSelect = '';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缩放功能
|
|
||||||
const zoomIn = () => {
|
|
||||||
const svg = getSvg();
|
|
||||||
if (svg) {
|
|
||||||
scale.value = Math.min(scale.value + 0.2, 10);
|
|
||||||
updateTransform(svg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoomOut = () => {
|
|
||||||
const svg = getSvg();
|
|
||||||
if (svg) {
|
|
||||||
scale.value = Math.max(scale.value - 0.2, 0.1);
|
|
||||||
updateTransform(svg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
const svg = getSvg();
|
|
||||||
if (svg) {
|
|
||||||
resetState();
|
|
||||||
updateTransform(svg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fullscreen = () => {
|
|
||||||
if (!container.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
container.value.requestFullscreen?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialize = () => {
|
|
||||||
if (!container.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
resetState();
|
|
||||||
|
|
||||||
const svg = getSvg();
|
|
||||||
if (svg) {
|
|
||||||
removeEvents = addDragEvents(svg);
|
|
||||||
updateTransform(svg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const destroy = () => {
|
|
||||||
removeEvents?.();
|
|
||||||
removeEvents = null;
|
|
||||||
resetState();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听容器变化
|
|
||||||
watch(
|
|
||||||
() => container.value,
|
|
||||||
() => {
|
|
||||||
destroy();
|
|
||||||
resetState();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 组件卸载时清理
|
|
||||||
onUnmounted(destroy);
|
|
||||||
|
|
||||||
return {
|
|
||||||
zoomIn,
|
|
||||||
zoomOut,
|
|
||||||
reset,
|
|
||||||
fullscreen,
|
|
||||||
destroy,
|
|
||||||
initialize
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import type { Pluggable } from 'unified';
|
|
||||||
import rehypeKatex from 'rehype-katex';
|
|
||||||
import rehypeRaw from 'rehype-raw';
|
|
||||||
import remarkBreaks from 'remark-breaks';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import remarkMath from 'remark-math';
|
|
||||||
import { computed, toRefs } from 'vue';
|
|
||||||
import { rehypeAnimatedPlugin } from '../plugins/rehypePlugin';
|
|
||||||
|
|
||||||
function usePlugins(props: any) {
|
|
||||||
const {
|
|
||||||
allowHtml,
|
|
||||||
enableAnimate,
|
|
||||||
enableLatex,
|
|
||||||
enableBreaks,
|
|
||||||
rehypePlugins,
|
|
||||||
remarkPlugins,
|
|
||||||
rehypePluginsAhead,
|
|
||||||
remarkPluginsAhead
|
|
||||||
} = toRefs(props);
|
|
||||||
|
|
||||||
const rehype = computed(() => {
|
|
||||||
return [
|
|
||||||
...(rehypePluginsAhead.value as Pluggable[]),
|
|
||||||
allowHtml.value && rehypeRaw,
|
|
||||||
enableLatex.value && rehypeKatex,
|
|
||||||
enableAnimate.value && rehypeAnimatedPlugin,
|
|
||||||
...(rehypePlugins.value as Pluggable[])
|
|
||||||
].filter(Boolean) as Pluggable[];
|
|
||||||
});
|
|
||||||
|
|
||||||
const remark = computed(() => {
|
|
||||||
const base: (Pluggable | { plugins: Pluggable[] })[] = [
|
|
||||||
enableLatex.value && remarkMath,
|
|
||||||
enableBreaks.value && remarkBreaks
|
|
||||||
].filter(Boolean) as (Pluggable | { plugins: Pluggable[] })[];
|
|
||||||
|
|
||||||
return [
|
|
||||||
[remarkGfm, { singleTilde: false }],
|
|
||||||
...(remarkPluginsAhead.value as (Pluggable | { plugins: Pluggable[] })[]),
|
|
||||||
...base,
|
|
||||||
...(remarkPlugins.value as (Pluggable | { plugins: Pluggable[] })[])
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
rehypePlugins: rehype,
|
|
||||||
remarkPlugins: remark
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export { usePlugins };
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import type { Root } from 'hast';
|
|
||||||
import type {
|
|
||||||
BundledHighlighterOptions,
|
|
||||||
CodeToHastOptions,
|
|
||||||
CodeToTokensBaseOptions,
|
|
||||||
CodeToTokensOptions,
|
|
||||||
CodeToTokensWithThemesOptions,
|
|
||||||
GrammarState,
|
|
||||||
HighlighterGeneric,
|
|
||||||
RequireKeys,
|
|
||||||
ThemedToken,
|
|
||||||
ThemedTokenWithVariants,
|
|
||||||
TokensResult
|
|
||||||
} from 'shiki';
|
|
||||||
import { GLOBAL_SHIKI_KEY } from '@components/XMarkdownCore/shared';
|
|
||||||
import {
|
|
||||||
createdBundledHighlighter,
|
|
||||||
createOnigurumaEngine,
|
|
||||||
createSingletonShorthands
|
|
||||||
} from 'shiki';
|
|
||||||
import { onUnmounted, provide, ref } from 'vue';
|
|
||||||
import { languageLoaders, themeLoaders } from '../../../hooks/shiki-loader';
|
|
||||||
|
|
||||||
export interface GlobalShiki {
|
|
||||||
codeToHtml: (
|
|
||||||
code: string,
|
|
||||||
options: CodeToHastOptions<string, string>
|
|
||||||
) => Promise<string>;
|
|
||||||
codeToHast: (
|
|
||||||
code: string,
|
|
||||||
options: CodeToHastOptions<string, string>
|
|
||||||
) => Promise<Root>;
|
|
||||||
codeToTokensBase: (
|
|
||||||
code: string,
|
|
||||||
options: RequireKeys<
|
|
||||||
CodeToTokensBaseOptions<string, string>,
|
|
||||||
'theme' | 'lang'
|
|
||||||
>
|
|
||||||
) => Promise<ThemedToken[][]>;
|
|
||||||
codeToTokens: (
|
|
||||||
code: string,
|
|
||||||
options: CodeToTokensOptions<string, string>
|
|
||||||
) => Promise<TokensResult>;
|
|
||||||
codeToTokensWithThemes: (
|
|
||||||
code: string,
|
|
||||||
options: RequireKeys<
|
|
||||||
CodeToTokensWithThemesOptions<string, string>,
|
|
||||||
'lang' | 'themes'
|
|
||||||
>
|
|
||||||
) => Promise<ThemedTokenWithVariants[][]>;
|
|
||||||
getSingletonHighlighter: (
|
|
||||||
options?: Partial<BundledHighlighterOptions<string, string>>
|
|
||||||
) => Promise<HighlighterGeneric<string, string>>;
|
|
||||||
getLastGrammarState:
|
|
||||||
| ((element: ThemedToken[][] | Root) => GrammarState)
|
|
||||||
| ((
|
|
||||||
code: string,
|
|
||||||
options: CodeToTokensBaseOptions<string, string>
|
|
||||||
) => Promise<GrammarState>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Shiki 管理器(单例 + 懒初始化)
|
|
||||||
*/
|
|
||||||
class ShikiManager {
|
|
||||||
private static instance: ShikiManager | null = null;
|
|
||||||
|
|
||||||
private shikiInstance: GlobalShiki | null = null;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
static getInstance(): ShikiManager {
|
|
||||||
if (!ShikiManager.instance) {
|
|
||||||
ShikiManager.instance = new ShikiManager();
|
|
||||||
}
|
|
||||||
return ShikiManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getShiki(): GlobalShiki {
|
|
||||||
if (this.shikiInstance) return this.shikiInstance;
|
|
||||||
|
|
||||||
const highlighterFactory = createdBundledHighlighter({
|
|
||||||
langs: languageLoaders,
|
|
||||||
themes: themeLoaders,
|
|
||||||
engine: () => createOnigurumaEngine(import('shiki/wasm'))
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
codeToHtml,
|
|
||||||
codeToHast,
|
|
||||||
codeToTokensBase,
|
|
||||||
codeToTokens,
|
|
||||||
codeToTokensWithThemes,
|
|
||||||
getSingletonHighlighter,
|
|
||||||
getLastGrammarState
|
|
||||||
} = createSingletonShorthands(highlighterFactory);
|
|
||||||
|
|
||||||
this.shikiInstance = {
|
|
||||||
codeToHtml,
|
|
||||||
codeToHast,
|
|
||||||
codeToTokensBase,
|
|
||||||
codeToTokens,
|
|
||||||
codeToTokensWithThemes,
|
|
||||||
getSingletonHighlighter,
|
|
||||||
getLastGrammarState
|
|
||||||
};
|
|
||||||
return this.shikiInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose() {
|
|
||||||
this.shikiInstance = null;
|
|
||||||
ShikiManager.instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局状态管理
|
|
||||||
let globalShikiInstance: GlobalShiki | undefined;
|
|
||||||
let globalShikiManager: ShikiManager | undefined;
|
|
||||||
let referenceCount = 0;
|
|
||||||
|
|
||||||
const shikiIsCreated = ref(false);
|
|
||||||
const shikiInstance = ref<GlobalShiki>();
|
|
||||||
const shikiManager = ref<ShikiManager>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 在 Vue 中提供 Shiki 实例(支持多组件实例)
|
|
||||||
*/
|
|
||||||
export function useShiki(): GlobalShiki {
|
|
||||||
// 增加引用计数
|
|
||||||
referenceCount++;
|
|
||||||
|
|
||||||
// ✅ 注册 onUnmounted 钩子
|
|
||||||
onUnmounted(() => {
|
|
||||||
referenceCount--;
|
|
||||||
console.log(`shiki reference count: ${referenceCount}`);
|
|
||||||
|
|
||||||
// 只有当所有组件都卸载时才清理
|
|
||||||
if (referenceCount === 0) {
|
|
||||||
console.log('shiki destroyed - all references removed');
|
|
||||||
shikiIsCreated.value = false;
|
|
||||||
shikiInstance.value = undefined;
|
|
||||||
shikiManager.value?.dispose();
|
|
||||||
globalShikiManager?.dispose();
|
|
||||||
globalShikiInstance = undefined;
|
|
||||||
globalShikiManager = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ 仅在首次时初始化
|
|
||||||
if (!globalShikiInstance) {
|
|
||||||
console.log('shiki created');
|
|
||||||
globalShikiManager = ShikiManager.getInstance();
|
|
||||||
globalShikiInstance = globalShikiManager.getShiki();
|
|
||||||
|
|
||||||
shikiManager.value = globalShikiManager;
|
|
||||||
shikiInstance.value = globalShikiInstance;
|
|
||||||
|
|
||||||
provide(GLOBAL_SHIKI_KEY, shikiInstance);
|
|
||||||
shikiIsCreated.value = true;
|
|
||||||
} else {
|
|
||||||
// 为后续组件实例提供相同的实例
|
|
||||||
shikiManager.value = globalShikiManager;
|
|
||||||
shikiInstance.value = globalShikiInstance;
|
|
||||||
provide(GLOBAL_SHIKI_KEY, shikiInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
return globalShikiInstance;
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import type {
|
|
||||||
BundledLanguage,
|
|
||||||
BundledTheme,
|
|
||||||
HighlighterGeneric,
|
|
||||||
ThemeRegistrationResolved
|
|
||||||
} from 'shiki';
|
|
||||||
import type { InitShikiOptions } from '../shared';
|
|
||||||
import { createHighlighter } from 'shiki';
|
|
||||||
import { shikiThemeDefault } from '../shared';
|
|
||||||
import { useDarkModeWatcher } from './useThemeMode';
|
|
||||||
|
|
||||||
interface UseShikiOptions {
|
|
||||||
themes?: InitShikiOptions['themes'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const highlighter =
|
|
||||||
shallowRef<HighlighterGeneric<BundledLanguage, BundledTheme>>();
|
|
||||||
const shikiThemeColor = ref<ThemeRegistrationResolved>();
|
|
||||||
const hasCreated = ref(false);
|
|
||||||
const oldThemes = ref<InitShikiOptions['themes']>();
|
|
||||||
|
|
||||||
export function useGlobalShikiHighlighter(options?: UseShikiOptions) {
|
|
||||||
const { isDark } = useDarkModeWatcher();
|
|
||||||
|
|
||||||
const themeArr = computed(() => {
|
|
||||||
if (options?.themes) {
|
|
||||||
return Object.keys(options.themes).map(key => options.themes![key]);
|
|
||||||
}
|
|
||||||
return [shikiThemeDefault.light, shikiThemeDefault.dark];
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateThemeColor = () => {
|
|
||||||
if (!highlighter.value || !hasCreated.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const themeName = isDark.value ? themeArr.value[1] : themeArr.value[0];
|
|
||||||
|
|
||||||
shikiThemeColor.value = highlighter.value.getTheme(themeName as any);
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
if (
|
|
||||||
hasCreated.value &&
|
|
||||||
JSON.stringify(oldThemes.value) === JSON.stringify(options?.themes)
|
|
||||||
) {
|
|
||||||
updateThemeColor();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const themes = [...themeArr.value];
|
|
||||||
if (!themes.length)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const newHighlighter = await createHighlighter({
|
|
||||||
themes: themes as any[],
|
|
||||||
langs: []
|
|
||||||
});
|
|
||||||
|
|
||||||
highlighter.value?.dispose?.();
|
|
||||||
highlighter.value = newHighlighter;
|
|
||||||
oldThemes.value = options?.themes;
|
|
||||||
hasCreated.value = true;
|
|
||||||
|
|
||||||
updateThemeColor();
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(isDark, updateThemeColor, { immediate: true });
|
|
||||||
|
|
||||||
const destroy = () => {
|
|
||||||
hasCreated.value = false;
|
|
||||||
highlighter.value?.dispose?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
highlighter,
|
|
||||||
shikiThemeColor,
|
|
||||||
isDark,
|
|
||||||
init,
|
|
||||||
destroy
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
|
|
||||||
export function useDarkModeWatcher() {
|
|
||||||
const isDark = ref(document.body.classList.contains('dark'));
|
|
||||||
|
|
||||||
let observer: MutationObserver;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
observer = new MutationObserver(() => {
|
|
||||||
isDark.value = document.body.classList.contains('dark');
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class'] // 只监听 class 变化
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
observer && observer.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDark
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './core';
|
|
||||||
export * from './MarkdownRender';
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
import type { Element, ElementContent, Root } from 'hast';
|
|
||||||
import type { BuildVisitor } from 'unist-util-visit';
|
|
||||||
import { visit } from 'unist-util-visit';
|
|
||||||
|
|
||||||
export function rehypeAnimatedPlugin() {
|
|
||||||
return (tree: Root) => {
|
|
||||||
visit(tree, 'element', ((node: Element) => {
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
'p',
|
|
||||||
'h1',
|
|
||||||
'h2',
|
|
||||||
'h3',
|
|
||||||
'h4',
|
|
||||||
'h5',
|
|
||||||
'h6',
|
|
||||||
'li',
|
|
||||||
'strong',
|
|
||||||
'th',
|
|
||||||
'td'
|
|
||||||
].includes(node.tagName) &&
|
|
||||||
node.children
|
|
||||||
) {
|
|
||||||
const newChildren: Array<ElementContent> = [];
|
|
||||||
for (const child of node.children) {
|
|
||||||
if (child.type === 'text') {
|
|
||||||
// @ts-expect-error Segmenter is not available in all environments
|
|
||||||
const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
|
|
||||||
const segments = segmenter.segment(child.value);
|
|
||||||
const words = [...segments]
|
|
||||||
.map(segment => segment.segment)
|
|
||||||
.filter(Boolean);
|
|
||||||
words.forEach((word: string) => {
|
|
||||||
newChildren.push({
|
|
||||||
children: [{ type: 'text', value: word }],
|
|
||||||
properties: {
|
|
||||||
className: 'x-markdown-animated-word'
|
|
||||||
},
|
|
||||||
tagName: 'span',
|
|
||||||
type: 'element'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
newChildren.push(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.children = newChildren;
|
|
||||||
}
|
|
||||||
}) as BuildVisitor<Root, 'element'>);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import type { GlobalShiki } from '@components/XMarkdownCore/hooks/useShiki';
|
|
||||||
import type { CodeXProps } from '@components/XMarkdownCore/shared/types';
|
|
||||||
import type { BuiltinTheme } from 'shiki';
|
|
||||||
import type { PluggableList } from 'unified';
|
|
||||||
import type { MermaidToolbarConfig } from '../components/Mermaid/types';
|
|
||||||
import type { ElxRunCodeOptions } from '../components/RunCode/type';
|
|
||||||
import type { CustomAttrs, SanitizeOptions } from '../core';
|
|
||||||
import type { InitShikiOptions } from './shikiHighlighter';
|
|
||||||
|
|
||||||
export const shikiThemeDefault: InitShikiOptions['themes'] = {
|
|
||||||
light: 'vitesse-light',
|
|
||||||
dark: 'vitesse-dark'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_PROPS = {
|
|
||||||
markdown: '',
|
|
||||||
allowHtml: false,
|
|
||||||
enableLatex: true,
|
|
||||||
enableAnimate: false,
|
|
||||||
enableBreaks: true,
|
|
||||||
codeXProps: () => ({}),
|
|
||||||
codeXRender: () => ({}),
|
|
||||||
codeXSlot: () => ({}),
|
|
||||||
codeHighlightTheme: null,
|
|
||||||
customAttrs: () => ({}),
|
|
||||||
remarkPlugins: () => [],
|
|
||||||
remarkPluginsAhead: () => [],
|
|
||||||
rehypePlugins: () => [],
|
|
||||||
rehypePluginsAhead: () => [],
|
|
||||||
rehypeOptions: () => ({}),
|
|
||||||
sanitize: false,
|
|
||||||
sanitizeOptions: () => ({}),
|
|
||||||
mermaidConfig: () => ({}),
|
|
||||||
langs: () => [],
|
|
||||||
defaultThemeMode: '' as 'light' | 'dark',
|
|
||||||
themes: () => ({ ...shikiThemeDefault }),
|
|
||||||
colorReplacements: () => ({}),
|
|
||||||
needViewCodeBtn: true,
|
|
||||||
secureViewCode: false,
|
|
||||||
viewCodeModalOptions: () => ({})
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MARKDOWN_CORE_PROPS = {
|
|
||||||
markdown: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
allowHtml: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
enableLatex: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
enableAnimate: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
enableBreaks: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
codeXProps: {
|
|
||||||
type: Object as PropType<CodeXProps>,
|
|
||||||
default: () => ({
|
|
||||||
enableCodePreview: false, // 启动代码预览功能
|
|
||||||
enableCodeCopy: true, // 启动代码复制功能
|
|
||||||
enableThemeToggle: false, // 启动主题切换
|
|
||||||
enableCodeLineNumber: false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
codeXRender: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
codeXSlot: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
codeHighlightTheme: {
|
|
||||||
type: Object as PropType<BuiltinTheme | null>,
|
|
||||||
default: () => null
|
|
||||||
},
|
|
||||||
customAttrs: {
|
|
||||||
type: Object as PropType<CustomAttrs>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
remarkPlugins: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
remarkPluginsAhead: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypePlugins: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypePluginsAhead: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypeOptions: {
|
|
||||||
type: Object as PropType<Record<string, any>>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
sanitize: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
sanitizeOptions: {
|
|
||||||
type: Object as PropType<SanitizeOptions>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
mermaidConfig: {
|
|
||||||
type: Object as PropType<Partial<MermaidToolbarConfig>>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
langs: {
|
|
||||||
type: Array as PropType<InitShikiOptions['langs']>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
defaultThemeMode: {
|
|
||||||
type: String as PropType<'light' | 'dark'>,
|
|
||||||
default: 'light'
|
|
||||||
},
|
|
||||||
themes: {
|
|
||||||
type: Object as PropType<InitShikiOptions['themes']>,
|
|
||||||
default: () =>
|
|
||||||
({
|
|
||||||
...shikiThemeDefault
|
|
||||||
}) satisfies InitShikiOptions['themes']
|
|
||||||
},
|
|
||||||
colorReplacements: {
|
|
||||||
type: Object as PropType<InitShikiOptions['colorReplacements']>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
needViewCodeBtn: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
secureViewCode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
viewCodeModalOptions: {
|
|
||||||
type: Object as PropType<ElxRunCodeOptions>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
isDark: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
globalShiki: {
|
|
||||||
type: Object as PropType<GlobalShiki>,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './markdownProvider';
|
|
||||||
// export * from './markdownRenderer';
|
|
||||||
export * from './shikiHighlighter';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const MARKDOWN_PROVIDER_KEY = Symbol('vue-element-plus-x-markdown-provider');
|
|
||||||
const GLOBAL_SHIKI_KEY = Symbol('vue-element-plus-x-markdown-shiki-provider');
|
|
||||||
|
|
||||||
const MERMAID_CACHE_KEY_LENGTH = 10000;
|
|
||||||
|
|
||||||
export { GLOBAL_SHIKI_KEY, MARKDOWN_PROVIDER_KEY, MERMAID_CACHE_KEY_LENGTH };
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import type { BuiltinTheme } from 'shiki';
|
|
||||||
import type { PluggableList } from 'unified';
|
|
||||||
import type { MermaidToolbarConfig } from '../components/Mermaid/types';
|
|
||||||
import type { CustomAttrs, SanitizeOptions } from '../core';
|
|
||||||
import type { InitShikiOptions } from './shikiHighlighter';
|
|
||||||
import { shikiThemeDefault } from './shikiHighlighter';
|
|
||||||
|
|
||||||
const MarkdownProps = {
|
|
||||||
markdown: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
allowHtml: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
enableCodeLineNumber: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
enableLatex: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
enableAnimate: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
enableBreaks: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
codeXRender: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
codeXSlot: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
codeHighlightTheme: {
|
|
||||||
type: Object as PropType<BuiltinTheme | null>,
|
|
||||||
default: () => null
|
|
||||||
},
|
|
||||||
customAttrs: {
|
|
||||||
type: Object as PropType<CustomAttrs>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
remarkPlugins: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
remarkPluginsAhead: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypePlugins: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypePluginsAhead: {
|
|
||||||
type: Array as PropType<PluggableList>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
rehypeOptions: {
|
|
||||||
type: Object as PropType<Record<string, any>>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
sanitize: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
sanitizeOptions: {
|
|
||||||
type: Object as PropType<SanitizeOptions>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
mermaidConfig: {
|
|
||||||
type: Object as PropType<Partial<MermaidToolbarConfig>>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
langs: {
|
|
||||||
type: Array as PropType<InitShikiOptions['langs']>,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
defaultThemeMode: {
|
|
||||||
type: String as PropType<'light' | 'dark'>,
|
|
||||||
default: 'light'
|
|
||||||
},
|
|
||||||
themes: {
|
|
||||||
type: Object as PropType<InitShikiOptions['themes']>,
|
|
||||||
default: () =>
|
|
||||||
({
|
|
||||||
...shikiThemeDefault
|
|
||||||
}) satisfies InitShikiOptions['themes']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export { MarkdownProps };
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
import type {
|
|
||||||
BundledLanguage,
|
|
||||||
BundledTheme,
|
|
||||||
LanguageInput,
|
|
||||||
StringLiteralUnion,
|
|
||||||
ThemeRegistrationAny
|
|
||||||
} from 'shiki';
|
|
||||||
|
|
||||||
// 初始化Shiki 高亮器配置
|
|
||||||
export interface InitShikiOptions {
|
|
||||||
// 语言列表
|
|
||||||
langs: Array<LanguageInput | BundledLanguage> | undefined;
|
|
||||||
// 主题列表
|
|
||||||
themes: Partial<
|
|
||||||
Record<
|
|
||||||
string | 'light' | 'dark',
|
|
||||||
ThemeRegistrationAny | StringLiteralUnion<BundledTheme, string>
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
/**
|
|
||||||
* 自定义当前主题下的代码颜色配置
|
|
||||||
*
|
|
||||||
* 一个颜色名称到新颜色值的映射表。
|
|
||||||
*
|
|
||||||
* 注意: 颜色的键必须以 `#` 开头,并且应为小写格式 ,否则不生效。
|
|
||||||
*
|
|
||||||
* 如果主题本身也定义了 `colorReplacements`,这个映射会与其合并。
|
|
||||||
*
|
|
||||||
* 最好和当前主题对应着修改
|
|
||||||
*
|
|
||||||
* @template
|
|
||||||
* ```typescript
|
|
||||||
* {
|
|
||||||
* "vitesse-light": {
|
|
||||||
* "#ab5959": "#ff66ff"
|
|
||||||
* },
|
|
||||||
* "vitesse-dark": {
|
|
||||||
* "#cb7676": "#ff0066"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
colorReplacements: Record<string, string | Record<string, string>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const shikiThemeDefault: InitShikiOptions['themes'] = {
|
|
||||||
light: 'vitesse-light',
|
|
||||||
dark: 'vitesse-dark'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SHIKI_SUPPORT_LANGS = [
|
|
||||||
'abap',
|
|
||||||
'actionscript-3',
|
|
||||||
'ada',
|
|
||||||
'apache',
|
|
||||||
'apex',
|
|
||||||
'apl',
|
|
||||||
'applescript',
|
|
||||||
'ara',
|
|
||||||
'asm',
|
|
||||||
'astro',
|
|
||||||
'awk',
|
|
||||||
'ballerina',
|
|
||||||
'bat',
|
|
||||||
'beancount',
|
|
||||||
'berry',
|
|
||||||
'bibtex',
|
|
||||||
'bicep',
|
|
||||||
'blade',
|
|
||||||
'c',
|
|
||||||
'cadence',
|
|
||||||
'clarity',
|
|
||||||
'clojure',
|
|
||||||
'cmake',
|
|
||||||
'cobol',
|
|
||||||
'codeql',
|
|
||||||
'coffee',
|
|
||||||
'cpp',
|
|
||||||
'crystal',
|
|
||||||
'csharp',
|
|
||||||
'css',
|
|
||||||
'cue',
|
|
||||||
'cypher',
|
|
||||||
'd',
|
|
||||||
'dart',
|
|
||||||
'dax',
|
|
||||||
'diff',
|
|
||||||
'docker',
|
|
||||||
'dream-maker',
|
|
||||||
'elixir',
|
|
||||||
'elm',
|
|
||||||
'erb',
|
|
||||||
'erlang',
|
|
||||||
'fish',
|
|
||||||
'fsharp',
|
|
||||||
'gdresource',
|
|
||||||
'gdscript',
|
|
||||||
'gdshader',
|
|
||||||
'gherkin',
|
|
||||||
'git-commit',
|
|
||||||
'git-rebase',
|
|
||||||
'glimmer-js',
|
|
||||||
'glimmer-ts',
|
|
||||||
'glsl',
|
|
||||||
'gnuplot',
|
|
||||||
'go',
|
|
||||||
'graphql',
|
|
||||||
'groovy',
|
|
||||||
'hack',
|
|
||||||
'haml',
|
|
||||||
'handlebars',
|
|
||||||
'haskell',
|
|
||||||
'hcl',
|
|
||||||
'hjson',
|
|
||||||
'hlsl',
|
|
||||||
'html',
|
|
||||||
'http',
|
|
||||||
'imba',
|
|
||||||
'ini',
|
|
||||||
'java',
|
|
||||||
'javascript',
|
|
||||||
'jinja-html',
|
|
||||||
'jison',
|
|
||||||
'json',
|
|
||||||
'json5',
|
|
||||||
'jsonc',
|
|
||||||
'jsonl',
|
|
||||||
'jsonnet',
|
|
||||||
'jssm',
|
|
||||||
'jsx',
|
|
||||||
'julia',
|
|
||||||
'kotlin',
|
|
||||||
'kusto',
|
|
||||||
'latex',
|
|
||||||
'less',
|
|
||||||
'liquid',
|
|
||||||
'lisp',
|
|
||||||
'logo',
|
|
||||||
'lua',
|
|
||||||
'make',
|
|
||||||
'markdown',
|
|
||||||
'marko',
|
|
||||||
'matlab',
|
|
||||||
'mdc',
|
|
||||||
'mdx',
|
|
||||||
'mermaid',
|
|
||||||
'mojo',
|
|
||||||
'narrat',
|
|
||||||
'nextflow',
|
|
||||||
'nginx',
|
|
||||||
'nim',
|
|
||||||
'nix',
|
|
||||||
'objective-c',
|
|
||||||
'objective-cpp',
|
|
||||||
'ocaml',
|
|
||||||
'pascal',
|
|
||||||
'perl',
|
|
||||||
'php',
|
|
||||||
'plsql',
|
|
||||||
'postcss',
|
|
||||||
'powerquery',
|
|
||||||
'powershell',
|
|
||||||
'prisma',
|
|
||||||
'prolog',
|
|
||||||
'proto',
|
|
||||||
'pug',
|
|
||||||
'puppet',
|
|
||||||
'purescript',
|
|
||||||
'python',
|
|
||||||
'r',
|
|
||||||
'raku',
|
|
||||||
'razor',
|
|
||||||
'reg',
|
|
||||||
'rel',
|
|
||||||
'riscv',
|
|
||||||
'rst',
|
|
||||||
'ruby',
|
|
||||||
'rust',
|
|
||||||
'sas',
|
|
||||||
'sass',
|
|
||||||
'scala',
|
|
||||||
'scheme',
|
|
||||||
'scss',
|
|
||||||
'shaderlab',
|
|
||||||
'shellscript',
|
|
||||||
'shellsession',
|
|
||||||
'smalltalk',
|
|
||||||
'solidity',
|
|
||||||
'sparql',
|
|
||||||
'splunk',
|
|
||||||
'sql',
|
|
||||||
'ssh-config',
|
|
||||||
'stata',
|
|
||||||
'stylus',
|
|
||||||
'svelte',
|
|
||||||
'swift',
|
|
||||||
'system-verilog',
|
|
||||||
'tasl',
|
|
||||||
'tcl',
|
|
||||||
'tex',
|
|
||||||
'toml',
|
|
||||||
'tsx',
|
|
||||||
'turtle',
|
|
||||||
'twig',
|
|
||||||
'typescript',
|
|
||||||
'v',
|
|
||||||
'vb',
|
|
||||||
'verilog',
|
|
||||||
'vhdl',
|
|
||||||
'viml',
|
|
||||||
'vue',
|
|
||||||
'vue-html',
|
|
||||||
'vyper',
|
|
||||||
'wasm',
|
|
||||||
'wenyan',
|
|
||||||
'wgsl',
|
|
||||||
'wolfram',
|
|
||||||
'xml',
|
|
||||||
'xsl',
|
|
||||||
'yaml',
|
|
||||||
'zenscript',
|
|
||||||
'zig',
|
|
||||||
'bash',
|
|
||||||
'batch',
|
|
||||||
'be',
|
|
||||||
'c#',
|
|
||||||
'cdc',
|
|
||||||
'clj',
|
|
||||||
'cmd',
|
|
||||||
'console',
|
|
||||||
'cql',
|
|
||||||
'cs',
|
|
||||||
'dockerfile',
|
|
||||||
'erl',
|
|
||||||
'f#',
|
|
||||||
'fs',
|
|
||||||
'fsl',
|
|
||||||
'gjs',
|
|
||||||
'gts',
|
|
||||||
'hbs',
|
|
||||||
'hs',
|
|
||||||
'jade',
|
|
||||||
'js',
|
|
||||||
'kql',
|
|
||||||
'makefile',
|
|
||||||
'md',
|
|
||||||
'nar',
|
|
||||||
'nf',
|
|
||||||
'objc',
|
|
||||||
'perl6',
|
|
||||||
'properties',
|
|
||||||
'ps',
|
|
||||||
'ps1',
|
|
||||||
'py',
|
|
||||||
'ql',
|
|
||||||
'rb',
|
|
||||||
'rs',
|
|
||||||
'sh',
|
|
||||||
'shader',
|
|
||||||
'shell',
|
|
||||||
'spl',
|
|
||||||
'styl',
|
|
||||||
'ts',
|
|
||||||
'vim',
|
|
||||||
'vimscript',
|
|
||||||
'vy',
|
|
||||||
'yml',
|
|
||||||
'zsh',
|
|
||||||
'文言'
|
|
||||||
] as const;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import type { TVueMarkdownProps } from '../';
|
|
||||||
import type { CodeBlockHeaderExpose } from '../components/CodeBlock/shiki-header';
|
|
||||||
import type { ElxRunCodeOptions } from '../components/RunCode/type';
|
|
||||||
import type { InitShikiOptions } from './shikiHighlighter';
|
|
||||||
|
|
||||||
export type MarkdownProps = {
|
|
||||||
allowHtml?: boolean;
|
|
||||||
enableLatex?: boolean;
|
|
||||||
enableAnimate?: boolean;
|
|
||||||
enableBreaks?: boolean;
|
|
||||||
codeXProps?: CodeXProps;
|
|
||||||
codeXRender?: Record<string, any>;
|
|
||||||
codeXSlot?: CodeBlockHeaderExpose & Record<string, any>;
|
|
||||||
codeHighlightTheme?: BuiltinTheme | null;
|
|
||||||
remarkPluginsAhead?: PluggableList;
|
|
||||||
rehypePluginsAhead?: PluggableList;
|
|
||||||
defaultThemeMode?: 'light' | 'dark';
|
|
||||||
needViewCodeBtn?: boolean;
|
|
||||||
secureViewCode?: boolean;
|
|
||||||
viewCodeModalOptions?: ElxRunCodeOptions;
|
|
||||||
mermaidConfig?: Partial<MermaidToolbarConfig>;
|
|
||||||
} & Partial<Pick<InitShikiOptions, 'langs' | 'themes' | 'colorReplacements'>> &
|
|
||||||
Pick<
|
|
||||||
TVueMarkdownProps,
|
|
||||||
| 'markdown'
|
|
||||||
| 'customAttrs'
|
|
||||||
| 'remarkPlugins'
|
|
||||||
| 'rehypePlugins'
|
|
||||||
| 'sanitize'
|
|
||||||
| 'sanitizeOptions'
|
|
||||||
| 'rehypeOptions'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type MarkdownProviderProps = Omit<MarkdownProps, 'markdown'> &
|
|
||||||
Partial<Pick<MarkdownProps, 'markdown'>>;
|
|
||||||
|
|
||||||
export interface CodeXProps {
|
|
||||||
enableCodePreview?: boolean; // 启动代码预览功能
|
|
||||||
enableCodeCopy?: boolean; // 启动代码复制功能
|
|
||||||
enableThemeToggle?: boolean; // 启动主题切换
|
|
||||||
enableCodeLineNumber?: boolean; // 开启行号
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
@keyframes fadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.x-markdown-animated-word {
|
|
||||||
animation: fadeIn 1s ease-in-out;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@import url('./katex.scss');
|
|
||||||
@import url('./animate.scss');
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.katex-error {
|
|
||||||
color: var(--el-text-color-secondary) !important;
|
|
||||||
}
|
|
||||||
.katex-html {
|
|
||||||
overflow: auto hidden;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
// // 媒体查询:当用户系统主题为暗色时 基于浏览器的主题色
|
|
||||||
// @media (prefers-color-scheme: light) {
|
|
||||||
// body {
|
|
||||||
// --shiki-custom-brr-mini: 3px;
|
|
||||||
// --shiki-custom-brr: 5px;
|
|
||||||
// --shiki-custom-blur: 10px;
|
|
||||||
// --shiki-code-header-bg: #fafafa;
|
|
||||||
// --shiki-code-header-span-color: #575757;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 媒体查询:当用户系统主题为暗色时
|
|
||||||
// @media (prefers-color-scheme: dark) {
|
|
||||||
// body {
|
|
||||||
// --shiki-custom-brr-mini: 3px;
|
|
||||||
// --shiki-custom-brr: 5px;
|
|
||||||
// --shiki-custom-blur: 10px;
|
|
||||||
// --shiki-code-header-bg: #272727;
|
|
||||||
// --shiki-code-header-span-color: #fafafa;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .shiki,
|
|
||||||
// .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;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
body {
|
|
||||||
--shiki-custom-brr-mini: 3px;
|
|
||||||
--shiki-custom-brr: 5px;
|
|
||||||
--shiki-custom-blur: 10px;
|
|
||||||
--shiki-code-header-bg: #fafafa;
|
|
||||||
--shiki-code-header-span-color: #575757;
|
|
||||||
--shiki-code-header-btn-bg: #ebedf0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark {
|
|
||||||
--shiki-custom-brr-mini: 3px;
|
|
||||||
--shiki-custom-brr: 5px;
|
|
||||||
--shiki-custom-blur: 10px;
|
|
||||||
--shiki-code-header-bg: #272727;
|
|
||||||
--shiki-code-header-span-color: #fafafa;
|
|
||||||
--shiki-code-header-btn-bg: #3b3b3b;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .shiki,
|
|
||||||
body.dark .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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiki {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.elx-xmarkdown-container {
|
|
||||||
// background-color: var(--el-fill-color);
|
|
||||||
color: black;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: var(--shiki-custom-brr);
|
|
||||||
|
|
||||||
/* 表格整体边框 */
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表头样式 */
|
|
||||||
th {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格行样式 */
|
|
||||||
td {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(odd) {
|
|
||||||
background-color: var(--el-fill-color-lighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre code {
|
|
||||||
text-shadow: none !important;
|
|
||||||
// 设置代码字体样式
|
|
||||||
span {
|
|
||||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
text-align: left;
|
|
||||||
// white-space: pre-wrap;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
tab-size: 4;
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre div.pre-md {
|
|
||||||
position: relative;
|
|
||||||
border-radius: var(--shiki-custom-brr-mini);
|
|
||||||
border: 1px solid var(--el-border-color);
|
|
||||||
min-width: 180px !important;
|
|
||||||
.markdown-elxLanguage-header-div {
|
|
||||||
box-sizing: content-box !important;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 5px 5px 5px 8px;
|
|
||||||
border-radius: var(--shiki-custom-brr-mini);
|
|
||||||
-webkit-backdrop-filter: blur(var(--shiki-custom-blur));
|
|
||||||
backdrop-filter: blur(var(--shiki-custom-blur));
|
|
||||||
margin: 0;
|
|
||||||
background-color: var(--shiki-code-header-bg);
|
|
||||||
z-index: 1;
|
|
||||||
span {
|
|
||||||
box-shadow: none !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-elxLanguage-header-span {
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
color: var(--shiki-code-header-span-color) !important;
|
|
||||||
font-weight: bold !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-elxLanguage-header-space-start,
|
|
||||||
.markdown-elxLanguage-header-space {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-elxLanguage-header-space-start {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiki-header-button {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: var(--shiki-custom-brr);
|
|
||||||
background-color: transparent;
|
|
||||||
width: fit-content !important;
|
|
||||||
padding: 0px 3px;
|
|
||||||
height: 24px;
|
|
||||||
opacity: 1;
|
|
||||||
transition: color 0.3s ease-in-out;
|
|
||||||
cursor: pointer;
|
|
||||||
.el-icon {
|
|
||||||
font-size: 15px !important;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
.el-icon,
|
|
||||||
span {
|
|
||||||
background-color: transparent !important;
|
|
||||||
color: var(--shiki-code-header-span-color) !important;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--shiki-code-header-btn-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 按钮图标的大小
|
|
||||||
.shiki-header-button {
|
|
||||||
span {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.shiki-header-button-expand {
|
|
||||||
span {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-elxLanguage-header-toggle-expand {
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
transition: transform 0.3s ease-in-out;
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.copied {
|
|
||||||
color: var(--el-color-success);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
flex: 1;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre div.is-expanded {
|
|
||||||
height: auto !important;
|
|
||||||
code {
|
|
||||||
padding: 8px !important;
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
.markdown-elxLanguage-header-toggle-expand {
|
|
||||||
transform: rotate(0deg) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/// <reference types="dom-speech-recognition" />
|
|
||||||
|
|
||||||
declare interface SpeechRecognition {
|
|
||||||
continuous: boolean;
|
|
||||||
interimResults: boolean;
|
|
||||||
lang: string;
|
|
||||||
onresult: (event: SpeechRecognitionEvent) => void;
|
|
||||||
onstart: () => void;
|
|
||||||
onend: () => void;
|
|
||||||
onerror: (event: SpeechRecognitionError) => void;
|
|
||||||
start: () => void;
|
|
||||||
stop: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare interface SpeechRecognitionEvent {
|
|
||||||
results: SpeechRecognitionResultList;
|
|
||||||
resultIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare interface SpeechRecognitionResultList {
|
|
||||||
[index: number]: SpeechRecognitionResult;
|
|
||||||
length: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare interface SpeechRecognitionResult {
|
|
||||||
[index: number]: SpeechRecognitionAlternative;
|
|
||||||
length: number;
|
|
||||||
isFinal: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare interface SpeechRecognitionAlternative {
|
|
||||||
confidence: number;
|
|
||||||
transcript: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare interface SpeechRecognitionError {
|
|
||||||
error: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare const webkitSpeechRecognition: {
|
|
||||||
new (): SpeechRecognition;
|
|
||||||
prototype: SpeechRecognition;
|
|
||||||
};
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
type OnError = (eventSource: EventSource, event: Event) => void;
|
|
||||||
|
|
||||||
type BaseFetchOptions = Omit<RequestInit, 'headers' | 'signal'> & {
|
|
||||||
headers?: HeadersInit | Headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Transformer<T> = (message: string) => T;
|
|
||||||
|
|
||||||
interface BaseSSEProps<T = string> {
|
|
||||||
baseURL?: string;
|
|
||||||
type?: SSEType;
|
|
||||||
onFinish?: (data: T[]) => void;
|
|
||||||
onAbort?: (data: T[]) => void;
|
|
||||||
transformer?: Transformer<T>;
|
|
||||||
onMessage?: (message: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SSEWithFetchProps {
|
|
||||||
baseOptions?: BaseFetchOptions;
|
|
||||||
onError?: (e: unknown) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SSEWithSSEProps {
|
|
||||||
baseOptions?: EventSourceInit;
|
|
||||||
onError?: OnError;
|
|
||||||
onOpen?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SSEType = 'fetch' | 'sse' | 'sip';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated 已经废弃, 请使用 hook-fetch 代替
|
|
||||||
* @deprecated This class has been deprecated, please use hook-fetch instead.
|
|
||||||
*
|
|
||||||
* @see {@link https://jsonlee12138.github.io/hook-fetch/ | hook-fetch 官方文档}
|
|
||||||
* @see {@link https://jsonlee12138.github.io/hook-fetch/ | hook-fetch Document}
|
|
||||||
*/
|
|
||||||
export type SSEProps<T> = BaseSSEProps<T> &
|
|
||||||
(SSEWithSSEProps | SSEWithFetchProps);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated 已经废弃, 请使用 hook-fetch 代替
|
|
||||||
* @deprecated This class has been deprecated, please use hook-fetch instead.
|
|
||||||
*
|
|
||||||
* @see {@link https://jsonlee12138.github.io/hook-fetch/ | hook-fetch 官方文档}
|
|
||||||
* @see {@link https://jsonlee12138.github.io/hook-fetch/ | hook-fetch Document}
|
|
||||||
*/
|
|
||||||
export class XRequest<T> {
|
|
||||||
#instance: EventSource | null = null;
|
|
||||||
#transformer?: Transformer<T>;
|
|
||||||
#baseURL: string;
|
|
||||||
#baseOptions?: EventSourceInit | BaseFetchOptions;
|
|
||||||
#onAbort?: BaseSSEProps<T>['onAbort'];
|
|
||||||
#onMessage?: BaseSSEProps<T>['onMessage'];
|
|
||||||
#onError?: SSEWithSSEProps['onError'] | SSEWithFetchProps['onError'];
|
|
||||||
#onOpen?: () => void;
|
|
||||||
#type: SSEType = 'sse';
|
|
||||||
#controller: AbortController | null = null;
|
|
||||||
#onFinish?: BaseSSEProps<T>['onFinish'];
|
|
||||||
#messages: T[] = [];
|
|
||||||
constructor({
|
|
||||||
baseURL,
|
|
||||||
onAbort,
|
|
||||||
onMessage,
|
|
||||||
onError,
|
|
||||||
baseOptions,
|
|
||||||
transformer,
|
|
||||||
type,
|
|
||||||
onFinish,
|
|
||||||
...props
|
|
||||||
}: SSEProps<T> = {}) {
|
|
||||||
this.#baseURL = baseURL ?? '';
|
|
||||||
this.#baseOptions = baseOptions ?? {};
|
|
||||||
onAbort && (this.#onAbort = onAbort);
|
|
||||||
onMessage && (this.#onMessage = onMessage);
|
|
||||||
onError && (this.#onError = onError);
|
|
||||||
onFinish && (this.#onFinish = onFinish);
|
|
||||||
(props as SSEWithSSEProps).onOpen &&
|
|
||||||
(this.#onOpen = (props as SSEWithSSEProps).onOpen);
|
|
||||||
transformer && (this.#transformer = transformer);
|
|
||||||
type && (this.#type = type);
|
|
||||||
this.abort = this.abort.bind(this);
|
|
||||||
this.send = this.send.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
#sendWithFetch(url: string, options: BaseFetchOptions = {}) {
|
|
||||||
this.#controller = new AbortController();
|
|
||||||
const signal = this.#controller.signal;
|
|
||||||
const fetchOptions = {
|
|
||||||
...options,
|
|
||||||
signal
|
|
||||||
} as RequestInit;
|
|
||||||
return fetch(this.#baseURL + url, fetchOptions)
|
|
||||||
.then(res => res.body)
|
|
||||||
.then(async body => {
|
|
||||||
if (!body) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error('Response body is null in stream mode')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const reader = body.getReader();
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
let done = false;
|
|
||||||
while (!done) {
|
|
||||||
const { value, done: streamDone } = await reader.read();
|
|
||||||
done = streamDone;
|
|
||||||
if (streamDone) {
|
|
||||||
this.#onFinish?.(this.#messages);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
|
||||||
const chunkUse = chunk.startsWith('data: ')
|
|
||||||
? chunk.slice(6)
|
|
||||||
: chunk;
|
|
||||||
try {
|
|
||||||
const res = this.#transformer
|
|
||||||
? (this.#transformer as Transformer<T>)(chunkUse)
|
|
||||||
: (chunkUse as T);
|
|
||||||
this.#messages.push(res);
|
|
||||||
this.#onMessage?.(res);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
(this.#onError as SSEWithFetchProps['onError'])?.(error);
|
|
||||||
this.#controller?.abort();
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.name === 'AbortError') {
|
|
||||||
this.#onAbort?.(this.#messages);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(this.#onError as SSEWithFetchProps['onError'])?.(err);
|
|
||||||
this.#controller?.abort();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#sendWithSSE(url: string, options: EventSourceInit = {}) {
|
|
||||||
const es = new EventSource(this.#baseURL + url, {
|
|
||||||
...this.#baseOptions,
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
es.onmessage = e => {
|
|
||||||
const res = this.#transformer
|
|
||||||
? this.#transformer(e.data)
|
|
||||||
: (e as MessageEvent<T>);
|
|
||||||
this.#onMessage?.(res as T);
|
|
||||||
};
|
|
||||||
es.onopen = () => {
|
|
||||||
this.#onOpen?.();
|
|
||||||
};
|
|
||||||
es.onerror = (ev: Event) => {
|
|
||||||
if (es.readyState === EventSource.CLOSED) {
|
|
||||||
this.#onFinish?.(this.#messages);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.#onError?.(es, ev);
|
|
||||||
}
|
|
||||||
this.abort();
|
|
||||||
};
|
|
||||||
this.#instance = es;
|
|
||||||
return es;
|
|
||||||
}
|
|
||||||
|
|
||||||
public send(url: string, options: EventSourceInit | BaseFetchOptions = {}) {
|
|
||||||
switch (this.#type) {
|
|
||||||
case 'fetch':
|
|
||||||
this.#sendWithFetch(url, options as BaseFetchOptions);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.#sendWithSSE(url, options as EventSourceInit);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abort() {
|
|
||||||
this.#instance?.close?.();
|
|
||||||
this.#instance = null;
|
|
||||||
this.#controller?.abort();
|
|
||||||
this.#controller = null;
|
|
||||||
this.#onAbort?.(this.#messages);
|
|
||||||
this.#messages = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from './usePrism';
|
|
||||||
export * from './useRecord';
|
|
||||||
export * from './useSend';
|
|
||||||
export * from './useXStream';
|
|
||||||
export * from './XRequest';
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
// 支持的语言映射表(你可以根据实际支持的语言增删)
|
|
||||||
export const languageLoaders: Record<string, () => Promise<any>> = {
|
|
||||||
abap: () => import('shiki/dist/langs/abap.mjs'),
|
|
||||||
'actionscript-3': () => import('shiki/dist/langs/actionscript-3.mjs'),
|
|
||||||
ada: () => import('shiki/dist/langs/ada.mjs'),
|
|
||||||
apache: () => import('shiki/dist/langs/apache.mjs'),
|
|
||||||
apex: () => import('shiki/dist/langs/apex.mjs'),
|
|
||||||
apl: () => import('shiki/dist/langs/apl.mjs'),
|
|
||||||
applescript: () => import('shiki/dist/langs/applescript.mjs'),
|
|
||||||
ara: () => import('shiki/dist/langs/ara.mjs'),
|
|
||||||
asm: () => import('shiki/dist/langs/asm.mjs'),
|
|
||||||
astro: () => import('shiki/dist/langs/astro.mjs'),
|
|
||||||
awk: () => import('shiki/dist/langs/awk.mjs'),
|
|
||||||
ballerina: () => import('shiki/dist/langs/ballerina.mjs'),
|
|
||||||
bash: () => import('shiki/dist/langs/bash.mjs'),
|
|
||||||
bat: () => import('shiki/dist/langs/bat.mjs'),
|
|
||||||
batch: () => import('shiki/dist/langs/batch.mjs'),
|
|
||||||
be: () => import('shiki/dist/langs/be.mjs'),
|
|
||||||
beancount: () => import('shiki/dist/langs/beancount.mjs'),
|
|
||||||
berry: () => import('shiki/dist/langs/berry.mjs'),
|
|
||||||
bibtex: () => import('shiki/dist/langs/bibtex.mjs'),
|
|
||||||
bicep: () => import('shiki/dist/langs/bicep.mjs'),
|
|
||||||
blade: () => import('shiki/dist/langs/blade.mjs'),
|
|
||||||
c: () => import('shiki/dist/langs/c.mjs'),
|
|
||||||
'c#': () => import('shiki/dist/langs/csharp.mjs'),
|
|
||||||
cadence: () => import('shiki/dist/langs/cadence.mjs'),
|
|
||||||
cdc: () => import('shiki/dist/langs/cdc.mjs'),
|
|
||||||
clarity: () => import('shiki/dist/langs/clarity.mjs'),
|
|
||||||
clj: () => import('shiki/dist/langs/clj.mjs'),
|
|
||||||
clojure: () => import('shiki/dist/langs/clojure.mjs'),
|
|
||||||
cmake: () => import('shiki/dist/langs/cmake.mjs'),
|
|
||||||
cmd: () => import('shiki/dist/langs/cmd.mjs'),
|
|
||||||
cobol: () => import('shiki/dist/langs/cobol.mjs'),
|
|
||||||
codeql: () => import('shiki/dist/langs/codeql.mjs'),
|
|
||||||
coffee: () => import('shiki/dist/langs/coffee.mjs'),
|
|
||||||
console: () => import('shiki/dist/langs/console.mjs'),
|
|
||||||
cpp: () => import('shiki/dist/langs/cpp.mjs'),
|
|
||||||
cql: () => import('shiki/dist/langs/cql.mjs'),
|
|
||||||
crystal: () => import('shiki/dist/langs/crystal.mjs'),
|
|
||||||
cs: () => import('shiki/dist/langs/cs.mjs'),
|
|
||||||
csharp: () => import('shiki/dist/langs/csharp.mjs'),
|
|
||||||
css: () => import('shiki/dist/langs/css.mjs'),
|
|
||||||
cue: () => import('shiki/dist/langs/cue.mjs'),
|
|
||||||
cypher: () => import('shiki/dist/langs/cypher.mjs'),
|
|
||||||
d: () => import('shiki/dist/langs/d.mjs'),
|
|
||||||
dart: () => import('shiki/dist/langs/dart.mjs'),
|
|
||||||
dax: () => import('shiki/dist/langs/dax.mjs'),
|
|
||||||
diff: () => import('shiki/dist/langs/diff.mjs'),
|
|
||||||
docker: () => import('shiki/dist/langs/docker.mjs'),
|
|
||||||
dockerfile: () => import('shiki/dist/langs/dockerfile.mjs'),
|
|
||||||
'dream-maker': () => import('shiki/dist/langs/dream-maker.mjs'),
|
|
||||||
elixir: () => import('shiki/dist/langs/elixir.mjs'),
|
|
||||||
elm: () => import('shiki/dist/langs/elm.mjs'),
|
|
||||||
erb: () => import('shiki/dist/langs/erb.mjs'),
|
|
||||||
erl: () => import('shiki/dist/langs/erl.mjs'),
|
|
||||||
erlang: () => import('shiki/dist/langs/erlang.mjs'),
|
|
||||||
'f#': () => import('shiki/dist/langs/fsharp.mjs'),
|
|
||||||
fish: () => import('shiki/dist/langs/fish.mjs'),
|
|
||||||
fs: () => import('shiki/dist/langs/fs.mjs'),
|
|
||||||
fsharp: () => import('shiki/dist/langs/fsharp.mjs'),
|
|
||||||
fsl: () => import('shiki/dist/langs/fsl.mjs'),
|
|
||||||
gdresource: () => import('shiki/dist/langs/gdresource.mjs'),
|
|
||||||
gdscript: () => import('shiki/dist/langs/gdscript.mjs'),
|
|
||||||
gdshader: () => import('shiki/dist/langs/gdshader.mjs'),
|
|
||||||
gherkin: () => import('shiki/dist/langs/gherkin.mjs'),
|
|
||||||
'git-commit': () => import('shiki/dist/langs/git-commit.mjs'),
|
|
||||||
'git-rebase': () => import('shiki/dist/langs/git-rebase.mjs'),
|
|
||||||
gjs: () => import('shiki/dist/langs/gjs.mjs'),
|
|
||||||
'glimmer-js': () => import('shiki/dist/langs/glimmer-js.mjs'),
|
|
||||||
'glimmer-ts': () => import('shiki/dist/langs/glimmer-ts.mjs'),
|
|
||||||
glsl: () => import('shiki/dist/langs/glsl.mjs'),
|
|
||||||
gnuplot: () => import('shiki/dist/langs/gnuplot.mjs'),
|
|
||||||
go: () => import('shiki/dist/langs/go.mjs'),
|
|
||||||
graphql: () => import('shiki/dist/langs/graphql.mjs'),
|
|
||||||
groovy: () => import('shiki/dist/langs/groovy.mjs'),
|
|
||||||
gts: () => import('shiki/dist/langs/gts.mjs'),
|
|
||||||
hack: () => import('shiki/dist/langs/hack.mjs'),
|
|
||||||
haml: () => import('shiki/dist/langs/haml.mjs'),
|
|
||||||
handlebars: () => import('shiki/dist/langs/handlebars.mjs'),
|
|
||||||
haskell: () => import('shiki/dist/langs/haskell.mjs'),
|
|
||||||
hbs: () => import('shiki/dist/langs/hbs.mjs'),
|
|
||||||
hcl: () => import('shiki/dist/langs/hcl.mjs'),
|
|
||||||
hjson: () => import('shiki/dist/langs/hjson.mjs'),
|
|
||||||
hlsl: () => import('shiki/dist/langs/hlsl.mjs'),
|
|
||||||
hs: () => import('shiki/dist/langs/hs.mjs'),
|
|
||||||
html: () => import('shiki/dist/langs/html.mjs'),
|
|
||||||
http: () => import('shiki/dist/langs/http.mjs'),
|
|
||||||
imba: () => import('shiki/dist/langs/imba.mjs'),
|
|
||||||
ini: () => import('shiki/dist/langs/ini.mjs'),
|
|
||||||
jade: () => import('shiki/dist/langs/jade.mjs'),
|
|
||||||
java: () => import('shiki/dist/langs/java.mjs'),
|
|
||||||
javascript: () => import('shiki/dist/langs/javascript.mjs'),
|
|
||||||
'jinja-html': () => import('shiki/dist/langs/jinja-html.mjs'),
|
|
||||||
jison: () => import('shiki/dist/langs/jison.mjs'),
|
|
||||||
js: () => import('shiki/dist/langs/js.mjs'),
|
|
||||||
json: () => import('shiki/dist/langs/json.mjs'),
|
|
||||||
json5: () => import('shiki/dist/langs/json5.mjs'),
|
|
||||||
jsonc: () => import('shiki/dist/langs/jsonc.mjs'),
|
|
||||||
jsonl: () => import('shiki/dist/langs/jsonl.mjs'),
|
|
||||||
jsonnet: () => import('shiki/dist/langs/jsonnet.mjs'),
|
|
||||||
jssm: () => import('shiki/dist/langs/jssm.mjs'),
|
|
||||||
jsx: () => import('shiki/dist/langs/jsx.mjs'),
|
|
||||||
julia: () => import('shiki/dist/langs/julia.mjs'),
|
|
||||||
kotlin: () => import('shiki/dist/langs/kotlin.mjs'),
|
|
||||||
kql: () => import('shiki/dist/langs/kql.mjs'),
|
|
||||||
kusto: () => import('shiki/dist/langs/kusto.mjs'),
|
|
||||||
latex: () => import('shiki/dist/langs/latex.mjs'),
|
|
||||||
less: () => import('shiki/dist/langs/less.mjs'),
|
|
||||||
liquid: () => import('shiki/dist/langs/liquid.mjs'),
|
|
||||||
lisp: () => import('shiki/dist/langs/lisp.mjs'),
|
|
||||||
logo: () => import('shiki/dist/langs/logo.mjs'),
|
|
||||||
lua: () => import('shiki/dist/langs/lua.mjs'),
|
|
||||||
make: () => import('shiki/dist/langs/make.mjs'),
|
|
||||||
makefile: () => import('shiki/dist/langs/makefile.mjs'),
|
|
||||||
markdown: () => import('shiki/dist/langs/markdown.mjs'),
|
|
||||||
marko: () => import('shiki/dist/langs/marko.mjs'),
|
|
||||||
matlab: () => import('shiki/dist/langs/matlab.mjs'),
|
|
||||||
md: () => import('shiki/dist/langs/md.mjs'),
|
|
||||||
mdc: () => import('shiki/dist/langs/mdc.mjs'),
|
|
||||||
mdx: () => import('shiki/dist/langs/mdx.mjs'),
|
|
||||||
mermaid: () => import('shiki/dist/langs/mermaid.mjs'),
|
|
||||||
mojo: () => import('shiki/dist/langs/mojo.mjs'),
|
|
||||||
nar: () => import('shiki/dist/langs/nar.mjs'),
|
|
||||||
narrat: () => import('shiki/dist/langs/narrat.mjs'),
|
|
||||||
nextflow: () => import('shiki/dist/langs/nextflow.mjs'),
|
|
||||||
nf: () => import('shiki/dist/langs/nf.mjs'),
|
|
||||||
nginx: () => import('shiki/dist/langs/nginx.mjs'),
|
|
||||||
nim: () => import('shiki/dist/langs/nim.mjs'),
|
|
||||||
nix: () => import('shiki/dist/langs/nix.mjs'),
|
|
||||||
objc: () => import('shiki/dist/langs/objc.mjs'),
|
|
||||||
'objective-c': () => import('shiki/dist/langs/objective-c.mjs'),
|
|
||||||
'objective-cpp': () => import('shiki/dist/langs/objective-cpp.mjs'),
|
|
||||||
ocaml: () => import('shiki/dist/langs/ocaml.mjs'),
|
|
||||||
pascal: () => import('shiki/dist/langs/pascal.mjs'),
|
|
||||||
perl: () => import('shiki/dist/langs/perl.mjs'),
|
|
||||||
perl6: () => import('shiki/dist/langs/perl6.mjs'),
|
|
||||||
php: () => import('shiki/dist/langs/php.mjs'),
|
|
||||||
plsql: () => import('shiki/dist/langs/plsql.mjs'),
|
|
||||||
postcss: () => import('shiki/dist/langs/postcss.mjs'),
|
|
||||||
powerquery: () => import('shiki/dist/langs/powerquery.mjs'),
|
|
||||||
powershell: () => import('shiki/dist/langs/powershell.mjs'),
|
|
||||||
prisma: () => import('shiki/dist/langs/prisma.mjs'),
|
|
||||||
prolog: () => import('shiki/dist/langs/prolog.mjs'),
|
|
||||||
properties: () => import('shiki/dist/langs/properties.mjs'),
|
|
||||||
proto: () => import('shiki/dist/langs/proto.mjs'),
|
|
||||||
ps: () => import('shiki/dist/langs/ps.mjs'),
|
|
||||||
ps1: () => import('shiki/dist/langs/ps1.mjs'),
|
|
||||||
pug: () => import('shiki/dist/langs/pug.mjs'),
|
|
||||||
puppet: () => import('shiki/dist/langs/puppet.mjs'),
|
|
||||||
purescript: () => import('shiki/dist/langs/purescript.mjs'),
|
|
||||||
py: () => import('shiki/dist/langs/py.mjs'),
|
|
||||||
python: () => import('shiki/dist/langs/python.mjs'),
|
|
||||||
ql: () => import('shiki/dist/langs/ql.mjs'),
|
|
||||||
r: () => import('shiki/dist/langs/r.mjs'),
|
|
||||||
raku: () => import('shiki/dist/langs/raku.mjs'),
|
|
||||||
razor: () => import('shiki/dist/langs/razor.mjs'),
|
|
||||||
rb: () => import('shiki/dist/langs/rb.mjs'),
|
|
||||||
reg: () => import('shiki/dist/langs/reg.mjs'),
|
|
||||||
rel: () => import('shiki/dist/langs/rel.mjs'),
|
|
||||||
riscv: () => import('shiki/dist/langs/riscv.mjs'),
|
|
||||||
rs: () => import('shiki/dist/langs/rs.mjs'),
|
|
||||||
rst: () => import('shiki/dist/langs/rst.mjs'),
|
|
||||||
ruby: () => import('shiki/dist/langs/ruby.mjs'),
|
|
||||||
rust: () => import('shiki/dist/langs/rust.mjs'),
|
|
||||||
sas: () => import('shiki/dist/langs/sas.mjs'),
|
|
||||||
sass: () => import('shiki/dist/langs/sass.mjs'),
|
|
||||||
scala: () => import('shiki/dist/langs/scala.mjs'),
|
|
||||||
scheme: () => import('shiki/dist/langs/scheme.mjs'),
|
|
||||||
scss: () => import('shiki/dist/langs/scss.mjs'),
|
|
||||||
sh: () => import('shiki/dist/langs/sh.mjs'),
|
|
||||||
shader: () => import('shiki/dist/langs/shader.mjs'),
|
|
||||||
shaderlab: () => import('shiki/dist/langs/shaderlab.mjs'),
|
|
||||||
shell: () => import('shiki/dist/langs/shell.mjs'),
|
|
||||||
shellscript: () => import('shiki/dist/langs/shellscript.mjs'),
|
|
||||||
shellsession: () => import('shiki/dist/langs/shellsession.mjs'),
|
|
||||||
smalltalk: () => import('shiki/dist/langs/smalltalk.mjs'),
|
|
||||||
solidity: () => import('shiki/dist/langs/solidity.mjs'),
|
|
||||||
sparql: () => import('shiki/dist/langs/sparql.mjs'),
|
|
||||||
spl: () => import('shiki/dist/langs/spl.mjs'),
|
|
||||||
splunk: () => import('shiki/dist/langs/splunk.mjs'),
|
|
||||||
sql: () => import('shiki/dist/langs/sql.mjs'),
|
|
||||||
'ssh-config': () => import('shiki/dist/langs/ssh-config.mjs'),
|
|
||||||
stata: () => import('shiki/dist/langs/stata.mjs'),
|
|
||||||
styl: () => import('shiki/dist/langs/styl.mjs'),
|
|
||||||
stylus: () => import('shiki/dist/langs/stylus.mjs'),
|
|
||||||
svelte: () => import('shiki/dist/langs/svelte.mjs'),
|
|
||||||
swift: () => import('shiki/dist/langs/swift.mjs'),
|
|
||||||
'system-verilog': () => import('shiki/dist/langs/system-verilog.mjs'),
|
|
||||||
tasl: () => import('shiki/dist/langs/tasl.mjs'),
|
|
||||||
tcl: () => import('shiki/dist/langs/tcl.mjs'),
|
|
||||||
tex: () => import('shiki/dist/langs/tex.mjs'),
|
|
||||||
toml: () => import('shiki/dist/langs/toml.mjs'),
|
|
||||||
ts: () => import('shiki/dist/langs/ts.mjs'),
|
|
||||||
tsx: () => import('shiki/dist/langs/tsx.mjs'),
|
|
||||||
turtle: () => import('shiki/dist/langs/turtle.mjs'),
|
|
||||||
twig: () => import('shiki/dist/langs/twig.mjs'),
|
|
||||||
typescript: () => import('shiki/dist/langs/typescript.mjs'),
|
|
||||||
v: () => import('shiki/dist/langs/v.mjs'),
|
|
||||||
vb: () => import('shiki/dist/langs/vb.mjs'),
|
|
||||||
verilog: () => import('shiki/dist/langs/verilog.mjs'),
|
|
||||||
vhdl: () => import('shiki/dist/langs/vhdl.mjs'),
|
|
||||||
vim: () => import('shiki/dist/langs/vim.mjs'),
|
|
||||||
viml: () => import('shiki/dist/langs/viml.mjs'),
|
|
||||||
vimscript: () => import('shiki/dist/langs/vimscript.mjs'),
|
|
||||||
vue: () => import('shiki/dist/langs/vue.mjs'),
|
|
||||||
'vue-html': () => import('shiki/dist/langs/vue-html.mjs'),
|
|
||||||
vy: () => import('shiki/dist/langs/vy.mjs'),
|
|
||||||
vyper: () => import('shiki/dist/langs/vyper.mjs'),
|
|
||||||
wasm: () => import('shiki/dist/langs/wasm.mjs'),
|
|
||||||
wenyan: () => import('shiki/dist/langs/wenyan.mjs'),
|
|
||||||
wgsl: () => import('shiki/dist/langs/wgsl.mjs'),
|
|
||||||
wolfram: () => import('shiki/dist/langs/wolfram.mjs'),
|
|
||||||
xml: () => import('shiki/dist/langs/xml.mjs'),
|
|
||||||
xsl: () => import('shiki/dist/langs/xsl.mjs'),
|
|
||||||
yaml: () => import('shiki/dist/langs/yaml.mjs'),
|
|
||||||
yml: () => import('shiki/dist/langs/yml.mjs'),
|
|
||||||
zenscript: () => import('shiki/dist/langs/zenscript.mjs'),
|
|
||||||
zig: () => import('shiki/dist/langs/zig.mjs'),
|
|
||||||
zsh: () => import('shiki/dist/langs/zsh.mjs'),
|
|
||||||
文言: () => import('shiki/dist/langs/wenyan.mjs')
|
|
||||||
};
|
|
||||||
|
|
||||||
export const themeLoaders: Record<string, () => Promise<any>> = {
|
|
||||||
andromeeda: () => import('shiki/dist/themes/andromeeda.mjs'),
|
|
||||||
'aurora-x': () => import('shiki/dist/themes/aurora-x.mjs'),
|
|
||||||
'ayu-dark': () => import('shiki/dist/themes/ayu-dark.mjs'),
|
|
||||||
'catppuccin-frappe': () => import('shiki/dist/themes/catppuccin-frappe.mjs'),
|
|
||||||
'catppuccin-latte': () => import('shiki/dist/themes/catppuccin-latte.mjs'),
|
|
||||||
'catppuccin-macchiato': () =>
|
|
||||||
import('shiki/dist/themes/catppuccin-macchiato.mjs'),
|
|
||||||
'catppuccin-mocha': () => import('shiki/dist/themes/catppuccin-mocha.mjs'),
|
|
||||||
'dark-plus': () => import('shiki/dist/themes/dark-plus.mjs'),
|
|
||||||
dracula: () => import('shiki/dist/themes/dracula.mjs'),
|
|
||||||
'dracula-soft': () => import('shiki/dist/themes/dracula-soft.mjs'),
|
|
||||||
'everforest-dark': () => import('shiki/dist/themes/everforest-dark.mjs'),
|
|
||||||
'everforest-light': () => import('shiki/dist/themes/everforest-light.mjs'),
|
|
||||||
'github-dark': () => import('shiki/dist/themes/github-dark.mjs'),
|
|
||||||
'github-dark-default': () =>
|
|
||||||
import('shiki/dist/themes/github-dark-default.mjs'),
|
|
||||||
'github-dark-dimmed': () =>
|
|
||||||
import('shiki/dist/themes/github-dark-dimmed.mjs'),
|
|
||||||
'github-dark-high-contrast': () =>
|
|
||||||
import('shiki/dist/themes/github-dark-high-contrast.mjs'),
|
|
||||||
'github-light': () => import('shiki/dist/themes/github-light.mjs'),
|
|
||||||
'github-light-default': () =>
|
|
||||||
import('shiki/dist/themes/github-light-default.mjs'),
|
|
||||||
'github-light-high-contrast': () =>
|
|
||||||
import('shiki/dist/themes/github-light-high-contrast.mjs'),
|
|
||||||
'gruvbox-dark-hard': () => import('shiki/dist/themes/gruvbox-dark-hard.mjs'),
|
|
||||||
'gruvbox-dark-medium': () =>
|
|
||||||
import('shiki/dist/themes/gruvbox-dark-medium.mjs'),
|
|
||||||
'gruvbox-dark-soft': () => import('shiki/dist/themes/gruvbox-dark-soft.mjs'),
|
|
||||||
'gruvbox-light-hard': () =>
|
|
||||||
import('shiki/dist/themes/gruvbox-light-hard.mjs'),
|
|
||||||
'gruvbox-light-medium': () =>
|
|
||||||
import('shiki/dist/themes/gruvbox-light-medium.mjs'),
|
|
||||||
'gruvbox-light-soft': () =>
|
|
||||||
import('shiki/dist/themes/gruvbox-light-soft.mjs'),
|
|
||||||
houston: () => import('shiki/dist/themes/houston.mjs'),
|
|
||||||
'kanagawa-dragon': () => import('shiki/dist/themes/kanagawa-dragon.mjs'),
|
|
||||||
'kanagawa-lotus': () => import('shiki/dist/themes/kanagawa-lotus.mjs'),
|
|
||||||
'kanagawa-wave': () => import('shiki/dist/themes/kanagawa-wave.mjs'),
|
|
||||||
laserwave: () => import('shiki/dist/themes/laserwave.mjs'),
|
|
||||||
'light-plus': () => import('shiki/dist/themes/light-plus.mjs'),
|
|
||||||
'material-theme': () => import('shiki/dist/themes/material-theme.mjs'),
|
|
||||||
'material-theme-darker': () =>
|
|
||||||
import('shiki/dist/themes/material-theme-darker.mjs'),
|
|
||||||
'material-theme-lighter': () =>
|
|
||||||
import('shiki/dist/themes/material-theme-lighter.mjs'),
|
|
||||||
'material-theme-ocean': () =>
|
|
||||||
import('shiki/dist/themes/material-theme-ocean.mjs'),
|
|
||||||
'material-theme-palenight': () =>
|
|
||||||
import('shiki/dist/themes/material-theme-palenight.mjs'),
|
|
||||||
'min-dark': () => import('shiki/dist/themes/min-dark.mjs'),
|
|
||||||
'min-light': () => import('shiki/dist/themes/min-light.mjs'),
|
|
||||||
monokai: () => import('shiki/dist/themes/monokai.mjs'),
|
|
||||||
'night-owl': () => import('shiki/dist/themes/night-owl.mjs'),
|
|
||||||
nord: () => import('shiki/dist/themes/nord.mjs'),
|
|
||||||
'one-dark-pro': () => import('shiki/dist/themes/one-dark-pro.mjs'),
|
|
||||||
'one-light': () => import('shiki/dist/themes/one-light.mjs'),
|
|
||||||
plastic: () => import('shiki/dist/themes/plastic.mjs'),
|
|
||||||
poimandres: () => import('shiki/dist/themes/poimandres.mjs'),
|
|
||||||
red: () => import('shiki/dist/themes/red.mjs'),
|
|
||||||
'rose-pine': () => import('shiki/dist/themes/rose-pine.mjs'),
|
|
||||||
'rose-pine-dawn': () => import('shiki/dist/themes/rose-pine-dawn.mjs'),
|
|
||||||
'rose-pine-moon': () => import('shiki/dist/themes/rose-pine-moon.mjs'),
|
|
||||||
'slack-dark': () => import('shiki/dist/themes/slack-dark.mjs'),
|
|
||||||
'slack-ochin': () => import('shiki/dist/themes/slack-ochin.mjs'),
|
|
||||||
'snazzy-light': () => import('shiki/dist/themes/snazzy-light.mjs'),
|
|
||||||
'solarized-dark': () => import('shiki/dist/themes/solarized-dark.mjs'),
|
|
||||||
'solarized-light': () => import('shiki/dist/themes/solarized-light.mjs'),
|
|
||||||
'synthwave-84': () => import('shiki/dist/themes/synthwave-84.mjs'),
|
|
||||||
'tokyo-night': () => import('shiki/dist/themes/tokyo-night.mjs'),
|
|
||||||
vesper: () => import('shiki/dist/themes/vesper.mjs'),
|
|
||||||
'vitesse-black': () => import('shiki/dist/themes/vitesse-black.mjs'),
|
|
||||||
'vitesse-dark': () => import('shiki/dist/themes/vitesse-dark.mjs'),
|
|
||||||
'vitesse-light': () => import('shiki/dist/themes/vitesse-light.mjs')
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import Prism from 'prismjs';
|
|
||||||
|
|
||||||
export function usePrism() {
|
|
||||||
const highlight = (code: string, lang: string) => {
|
|
||||||
try {
|
|
||||||
const grammar = Prism.languages[lang];
|
|
||||||
if (grammar) {
|
|
||||||
return Prism.highlight(code, grammar, lang);
|
|
||||||
}
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return highlight;
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
export interface UseRecordError {
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseRecordProps {
|
|
||||||
onError?: (err: SpeechRecognitionErrorEvent | UseRecordError) => void;
|
|
||||||
onStart?: () => void;
|
|
||||||
onEnd?: (v: string) => void;
|
|
||||||
onResult?: (result: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Provides a hook for speech recognition, allowing voice input and handling various events such as start, end, result, and errors.
|
|
||||||
* @description 提供语音识别的钩子,允许语音输入并处理开始、结束、结果及错误等各种事件。
|
|
||||||
*
|
|
||||||
* @interface UseRecordError
|
|
||||||
* Represents the structure of an error object for useRecord.
|
|
||||||
* 表示 useRecord 的错误对象结构。
|
|
||||||
*
|
|
||||||
* @property {number} code - The error code.
|
|
||||||
* @property {number} code - 错误代码。
|
|
||||||
*
|
|
||||||
* @property {string} message - The error message.
|
|
||||||
* @property {string} message - 错误信息。
|
|
||||||
*
|
|
||||||
* @interface UseRecordProps
|
|
||||||
* Represents the configuration options for useRecord.
|
|
||||||
* 表示 useRecord 的配置选项。
|
|
||||||
*
|
|
||||||
* @property {Function} [onError] - Callback function triggered when an error occurs.
|
|
||||||
* @property {Function} [onError] - 当发生错误时触发的回调函数。
|
|
||||||
*
|
|
||||||
* @property {Function} [onStart] - Callback function triggered when voice recognition starts.
|
|
||||||
* @property {Function} [onStart] - 当语音识别开始时触发的回调函数。
|
|
||||||
*
|
|
||||||
* @property {Function} [onEnd] - Callback function triggered when voice recognition ends, providing the final recognized text.
|
|
||||||
* @property {Function} [onEnd] - 当语音识别结束时触发的回调函数,并提供最终识别的文本。
|
|
||||||
*
|
|
||||||
* @property {Function} [onResult] - Callback function triggered when intermediate recognition results are available.
|
|
||||||
* @property {Function} [onResult] - 当有中间识别结果时触发的回调函数。
|
|
||||||
*
|
|
||||||
* @function useRecord
|
|
||||||
* Initializes the speech recognition functionality and returns state and control functions.
|
|
||||||
* 初始化语音识别功能并返回状态和控制函数。
|
|
||||||
*
|
|
||||||
* @param {UseRecordProps} [props] - Configuration options for the hook.
|
|
||||||
* @param {UseRecordProps} [props] - 钩子的配置选项。
|
|
||||||
*
|
|
||||||
* @returns {object} - An object containing the state and methods for speech recognition.
|
|
||||||
* @returns {object} - 包含语音识别状态和方法的对象。
|
|
||||||
*
|
|
||||||
* @property {Ref<boolean>} loading - Indicates whether speech recognition is currently active.
|
|
||||||
* @property {Ref<boolean>} loading - 指示语音识别是否正在进行。
|
|
||||||
*
|
|
||||||
* @property {Function} start - Starts the speech recognition process.
|
|
||||||
* @property {Function} start - 开始语音识别过程。
|
|
||||||
*
|
|
||||||
* @property {Function} stop - Stops the speech recognition process.
|
|
||||||
* @property {Function} stop - 停止语音识别过程。
|
|
||||||
*
|
|
||||||
* @property {Ref<string>} value - Holds the recognized text.
|
|
||||||
* @property {Ref<string>} value - 保存识别的文本。
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { loading, start, stop, value } = useRecord({
|
|
||||||
* onStart: () => console.log('Recognition started'),
|
|
||||||
* onEnd: (result) => console.log('Recognition ended with result:', result),
|
|
||||||
* onResult: (result) => console.log('Intermediate result:', result),
|
|
||||||
* onError: (error) => console.error('Error:', error),
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* start(); // Start voice recognition
|
|
||||||
* stop(); // Stop voice recognition
|
|
||||||
* console.log(value.value); // Access the recognized text
|
|
||||||
*/
|
|
||||||
export function useRecord({
|
|
||||||
onError,
|
|
||||||
onStart,
|
|
||||||
onEnd,
|
|
||||||
onResult,
|
|
||||||
}: UseRecordProps = {}) {
|
|
||||||
const loading = ref<boolean>(false);
|
|
||||||
const recognition = ref<null | SpeechRecognition>(null);
|
|
||||||
const value = ref<string>('');
|
|
||||||
|
|
||||||
const start = () => {
|
|
||||||
if ('webkitSpeechRecognition' in window) {
|
|
||||||
recognition.value = new webkitSpeechRecognition();
|
|
||||||
recognition.value!.continuous = true;
|
|
||||||
recognition.value.interimResults = true;
|
|
||||||
recognition.value.lang = 'zh-CN';
|
|
||||||
recognition.value.onstart = () => {
|
|
||||||
loading.value = true;
|
|
||||||
value.value = '';
|
|
||||||
onStart?.();
|
|
||||||
};
|
|
||||||
recognition.value.onend = () => {
|
|
||||||
loading.value = false;
|
|
||||||
onEnd?.(value.value);
|
|
||||||
};
|
|
||||||
recognition.value.onerror = (e) => {
|
|
||||||
loading.value = false;
|
|
||||||
onError?.(e);
|
|
||||||
};
|
|
||||||
recognition.value.onresult = (e) => {
|
|
||||||
let results = '';
|
|
||||||
for (let i = 0; i <= e.resultIndex; i++) {
|
|
||||||
results += e.results[i][0].transcript;
|
|
||||||
}
|
|
||||||
value.value = results;
|
|
||||||
onResult?.(results);
|
|
||||||
};
|
|
||||||
recognition.value.start();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
onError?.({
|
|
||||||
code: -1,
|
|
||||||
message: 'The current browser does not support voice recognition',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
if (recognition.value) {
|
|
||||||
recognition.value.stop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stop();
|
|
||||||
recognition.value = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
export interface UseSendProps {
|
|
||||||
onAbort?: () => void;
|
|
||||||
sendHandler?: (...args: any[]) => void;
|
|
||||||
abortHandler?: () => void;
|
|
||||||
finishHandler?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description A utility function for handling the request status of send operation management, supporting optional abort functionality, as well as Promise and Server-Sent Events (SSE).
|
|
||||||
* @description 一个用于处理发送操作管理请求状态的实用函数,支持可选的中止功能,同时支持 Promise 和 SSE(服务端事件)。
|
|
||||||
*
|
|
||||||
* @typedef {object} WithAbortProps
|
|
||||||
* @property {(signal: AbortSignal) => Promise<any>} promise - A function that returns a promise and accepts an `AbortSignal` for cancellation.
|
|
||||||
* @property {(signal: AbortSignal) => Promise<any>} promise - 一个返回 Promise 的函数,接受一个用于取消的 `AbortSignal`。
|
|
||||||
*
|
|
||||||
* @typedef {object} WithSSEProps
|
|
||||||
* @property {EventSource} eventSource - An `EventSource` instance for handling Server-Sent Events.
|
|
||||||
* @property {EventSource} eventSource - 用于处理服务端事件的 `EventSource` 实例。
|
|
||||||
*
|
|
||||||
* @typedef {object} UseSendProps
|
|
||||||
* @property {WithAbortProps | WithSSEProps} props - Either `WithAbortProps` or `WithSSEProps`, depending on the use case.
|
|
||||||
* @property {WithAbortProps | WithSSEProps} props - 根据使用场景,传入 `WithAbortProps` 或 `WithSSEProps`。
|
|
||||||
* @property {() => void} [onAbort] - Optional callback triggered when the operation is aborted.
|
|
||||||
* @property {() => void} [onAbort] - 可选的回调函数,在操作被中止时触发。
|
|
||||||
* @property {(...args: any[]) => void} [sendHandler] - Optional handler function invoked before sending starts.
|
|
||||||
* @property {(...args: any[]) => void} [sendHandler] - 可选的处理函数,在发送开始前调用。
|
|
||||||
*
|
|
||||||
* @param {UseSendProps} props - Configuration options for the `useSend` function.
|
|
||||||
* @param {UseSendProps} props - `useSend` 函数的配置选项。
|
|
||||||
*
|
|
||||||
* @returns {object} - Returns an object containing utility methods and state.
|
|
||||||
* @returns {object} - 返回一个包含实用方法和状态的对象。
|
|
||||||
*
|
|
||||||
* @property {Ref<boolean>} loading - A reactive reference indicating whether a send operation is in progress.
|
|
||||||
* @property {Ref<boolean>} loading - 一个响应式引用,指示是否正在执行发送操作。
|
|
||||||
*
|
|
||||||
* @property {Promise<any> | undefined} promise - The promise returned by the `promise` function, if provided.
|
|
||||||
* @property {Promise<any> | undefined} promise - 如果提供了 `promise` 函数,则返回的 Promise。
|
|
||||||
*
|
|
||||||
* @property {() => void} abort - A function to abort the ongoing operation, either by aborting the promise or closing the EventSource.
|
|
||||||
* @property {() => void} abort - 一个用于中止当前操作的函数,可以中止 Promise 或关闭 EventSource。
|
|
||||||
*
|
|
||||||
* @property {(...args: any[]) => void} send - A function to initiate the send operation, invoking the `sendHandler` if provided.
|
|
||||||
* @property {(...args: any[]) => void} send - 一个用于启动发送操作的函数,如果提供了 `sendHandler` 则会调用。
|
|
||||||
*/
|
|
||||||
export function useSend(
|
|
||||||
{ onAbort, sendHandler, abortHandler }: UseSendProps = {} as UseSendProps
|
|
||||||
) {
|
|
||||||
const loading = ref<boolean>(false);
|
|
||||||
|
|
||||||
const handleSend = (...args: any[]) => {
|
|
||||||
if (loading.value)
|
|
||||||
return;
|
|
||||||
sendHandler?.(...args);
|
|
||||||
loading.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAbort = () => {
|
|
||||||
loading.value = false;
|
|
||||||
abortHandler?.();
|
|
||||||
onAbort?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = () => {
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
abort: handleAbort,
|
|
||||||
send: handleSend,
|
|
||||||
finish: handleFinish
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
const DEFAULT_STREAM_SEPARATOR = '\n\n';
|
|
||||||
const DEFAULT_PART_SEPARATOR = '\n';
|
|
||||||
const DEFAULT_KV_SEPARATOR = ':';
|
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
const isValidString = (str: string) => (str ?? '').trim() !== '';
|
|
||||||
|
|
||||||
// TransformStream 实现
|
|
||||||
function splitStream() {
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
return new TransformStream<string, string>({
|
|
||||||
transform(chunk, controller) {
|
|
||||||
buffer += chunk;
|
|
||||||
const parts = buffer.split(DEFAULT_STREAM_SEPARATOR);
|
|
||||||
parts.slice(0, -1).forEach((part) => {
|
|
||||||
if (isValidString(part))
|
|
||||||
controller.enqueue(part);
|
|
||||||
});
|
|
||||||
buffer = parts[parts.length - 1];
|
|
||||||
},
|
|
||||||
flush(controller) {
|
|
||||||
if (isValidString(buffer))
|
|
||||||
controller.enqueue(buffer);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitPart() {
|
|
||||||
return new TransformStream<string, SSEOutput>({
|
|
||||||
transform(partChunk, controller) {
|
|
||||||
const lines = partChunk.split(DEFAULT_PART_SEPARATOR);
|
|
||||||
const sseEvent = lines.reduce<SSEOutput>((acc, line) => {
|
|
||||||
const sepIndex = line.indexOf(DEFAULT_KV_SEPARATOR);
|
|
||||||
if (sepIndex === -1)
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
const key = line.slice(0, sepIndex);
|
|
||||||
if (!isValidString(key))
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
const value = line.slice(sepIndex + 1);
|
|
||||||
return { ...acc, [key]: value };
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
if (Object.keys(sseEvent).length > 0)
|
|
||||||
controller.enqueue(sseEvent);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 类型定义
|
|
||||||
export type SSEFields = 'data' | 'event' | 'id' | 'retry';
|
|
||||||
export type SSEOutput = Partial<Record<SSEFields, any>>;
|
|
||||||
|
|
||||||
export interface XStreamOptions<Output = SSEOutput> {
|
|
||||||
readableStream: ReadableStream<Uint8Array>;
|
|
||||||
transformStream?: TransformStream<string, Output>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 可读流类型,支持异步迭代和中断
|
|
||||||
type XReadableStream<R = SSEOutput> = ReadableStream<R> & {
|
|
||||||
[Symbol.asyncIterator]: () => AsyncGenerator<R>;
|
|
||||||
reader?: ReadableStreamDefaultReader<R>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 核心流处理函数(支持中断)
|
|
||||||
function XStream<Output = SSEOutput>(
|
|
||||||
options: XStreamOptions<Output>,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): XReadableStream<Output> {
|
|
||||||
const { readableStream, transformStream } = options;
|
|
||||||
if (!(readableStream instanceof ReadableStream)) {
|
|
||||||
throw new TypeError('options.readableStream 必须是 ReadableStream 的实例。');
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoderStream = new TextDecoderStream();
|
|
||||||
const processedStream = transformStream
|
|
||||||
? readableStream
|
|
||||||
.pipeThrough(decoderStream)
|
|
||||||
.pipeThrough(transformStream)
|
|
||||||
: readableStream
|
|
||||||
.pipeThrough(decoderStream)
|
|
||||||
.pipeThrough(splitStream())
|
|
||||||
.pipeThrough(splitPart()) as XReadableStream<Output>;
|
|
||||||
|
|
||||||
// 为流添加异步迭代器并处理中断信号
|
|
||||||
(processedStream as XReadableStream<Output>)[Symbol.asyncIterator] = async function* () {
|
|
||||||
const reader = this.getReader();
|
|
||||||
(this as XReadableStream<Output>).reader = reader; // 保存读取器引用
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
await reader.cancel(); // 主动取消 reader
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done)
|
|
||||||
break;
|
|
||||||
if (value)
|
|
||||||
yield value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
reader.releaseLock(); // 释放锁
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return processedStream as XReadableStream<Output>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vue3 Hooks 实现
|
|
||||||
export function useXStream() {
|
|
||||||
const data = ref<SSEOutput[]>([]);
|
|
||||||
const error = ref<Error | null>(null);
|
|
||||||
const isLoading = ref<boolean>(false);
|
|
||||||
const abortController = shallowRef<AbortController | null>(null);
|
|
||||||
const currentStream = shallowRef<XReadableStream<SSEOutput> | null>(null);
|
|
||||||
|
|
||||||
// 启动流式请求
|
|
||||||
const startStream = async (options: XStreamOptions<SSEOutput>) => {
|
|
||||||
isLoading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
data.value = [];
|
|
||||||
abortController.value = new AbortController();
|
|
||||||
currentStream.value = XStream(options, abortController.value.signal);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (const item of currentStream.value!) {
|
|
||||||
data.value.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
error.value = err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
currentStream.value = null; // 释放流引用
|
|
||||||
abortController.value = null; // 释放控制器
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 中断流式请求(强制关闭流)
|
|
||||||
const cancel = () => {
|
|
||||||
if (abortController.value) {
|
|
||||||
abortController.value.abort();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
startStream,
|
|
||||||
cancel, // 新增中断方法
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import type { App, Plugin } from 'vue';
|
|
||||||
import Attachments from './components/Attachments/index.vue';
|
|
||||||
import Bubble from './components/Bubble/index.vue';
|
|
||||||
import BubbleList from './components/BubbleList/index.vue';
|
|
||||||
import ConfigProvider from './components/ConfigProvider/index.vue';
|
|
||||||
import Conversations from './components/Conversations/index.vue';
|
|
||||||
import EditorSender from './components/EditorSender/index.vue';
|
|
||||||
import FilesCard from './components/FilesCard/index.vue';
|
|
||||||
import MentionSender from './components/MentionSender/index.vue';
|
|
||||||
import Prompts from './components/Prompts/index.vue';
|
|
||||||
import Sender from './components/Sender/index.vue';
|
|
||||||
import Thinking from './components/Thinking/index.vue';
|
|
||||||
import ThoughtChain from './components/ThoughtChain/index.vue';
|
|
||||||
import Typewriter from './components/Typewriter/index.vue';
|
|
||||||
import Welcome from './components/Welcome/index.vue';
|
|
||||||
import XMarkdown from './components/XMarkdown/index.vue';
|
|
||||||
import XMarkdownAsync from './components/XMarkdownAsync/index.vue';
|
|
||||||
|
|
||||||
export * from './components';
|
|
||||||
export * from './hooks';
|
|
||||||
|
|
||||||
const ElementPlusX: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
app.component('Attachments', Attachments);
|
|
||||||
app.component('Bubble', Bubble);
|
|
||||||
app.component('BubbleList', BubbleList);
|
|
||||||
app.component('ConfigProvider', ConfigProvider);
|
|
||||||
app.component('Conversations', Conversations);
|
|
||||||
app.component('EditorSender', EditorSender);
|
|
||||||
app.component('FilesCard', FilesCard);
|
|
||||||
app.component('MentionSender', MentionSender);
|
|
||||||
app.component('Prompts', Prompts);
|
|
||||||
app.component('Sender', Sender);
|
|
||||||
app.component('Thinking', Thinking);
|
|
||||||
app.component('ThoughtChain', ThoughtChain);
|
|
||||||
app.component('Typewriter', Typewriter);
|
|
||||||
app.component('Welcome', Welcome);
|
|
||||||
app.component('XMarkdown', XMarkdown);
|
|
||||||
app.component('XMarkdownAsync', XMarkdownAsync);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ElementPlusX;
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
import type { FilesType } from '../components/FilesCard/types.d.ts';
|
|
||||||
import { prefix } from '../components/Typewriter/config/index.ts';
|
|
||||||
|
|
||||||
/* 公共 相关 开始 */
|
|
||||||
/* 公共 相关 结束 */
|
|
||||||
|
|
||||||
/* FileCard 组件相关 开始 */
|
|
||||||
// 更据文件后缀名获取文件类型
|
|
||||||
export function getFileType(fileExtension: string): {
|
|
||||||
lowerCase: FilesType;
|
|
||||||
upperCase: string;
|
|
||||||
} {
|
|
||||||
// 去除后缀名开头的点,并转换为小写
|
|
||||||
const cleanExtension = fileExtension.replace('.', '').toLowerCase();
|
|
||||||
if (!cleanExtension) {
|
|
||||||
return { lowerCase: 'unknown', upperCase: 'Unknown' };
|
|
||||||
}
|
|
||||||
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'];
|
|
||||||
const wordExtensions = ['doc', 'docx'];
|
|
||||||
const excelExtensions = ['xls', 'xlsx'];
|
|
||||||
const pptExtensions = ['ppt', 'pptx'];
|
|
||||||
const audioExtensions = ['mp3', 'wav', 'ogg', 'flac'];
|
|
||||||
const videoExtensions = ['mp4', 'avi', 'mov', 'mkv'];
|
|
||||||
const codeExtensions = [
|
|
||||||
'js',
|
|
||||||
'ts',
|
|
||||||
'html',
|
|
||||||
'css',
|
|
||||||
'py',
|
|
||||||
'java',
|
|
||||||
'c',
|
|
||||||
'cpp',
|
|
||||||
'json',
|
|
||||||
'php'
|
|
||||||
];
|
|
||||||
const databaseExtensions = ['sql', 'db', 'sqlite'];
|
|
||||||
const zipExtensions = ['zip', 'rar', '7z'];
|
|
||||||
const markExtensions = ['md', 'mdx'];
|
|
||||||
|
|
||||||
if (imageExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'image', upperCase: 'Image' };
|
|
||||||
}
|
|
||||||
if (wordExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'word', upperCase: 'Word' };
|
|
||||||
}
|
|
||||||
if (excelExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'excel', upperCase: 'Excel' };
|
|
||||||
}
|
|
||||||
if (pptExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'ppt', upperCase: 'Ppt' };
|
|
||||||
}
|
|
||||||
if (cleanExtension === 'pdf') {
|
|
||||||
return { lowerCase: 'pdf', upperCase: 'Pdf' };
|
|
||||||
}
|
|
||||||
if (cleanExtension === 'txt') {
|
|
||||||
return { lowerCase: 'txt', upperCase: 'Txt' };
|
|
||||||
}
|
|
||||||
if (markExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'mark', upperCase: 'Markdown' };
|
|
||||||
}
|
|
||||||
if (audioExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'audio', upperCase: 'Audio' };
|
|
||||||
}
|
|
||||||
if (videoExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'video', upperCase: 'Video' };
|
|
||||||
}
|
|
||||||
if (codeExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'code', upperCase: 'Code' };
|
|
||||||
}
|
|
||||||
if (databaseExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'database', upperCase: 'Database' };
|
|
||||||
}
|
|
||||||
if (cleanExtension === 'lnk') {
|
|
||||||
return { lowerCase: 'link', upperCase: 'Link' };
|
|
||||||
}
|
|
||||||
if (zipExtensions.includes(cleanExtension)) {
|
|
||||||
return { lowerCase: 'zip', upperCase: 'Zip' };
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
cleanExtension === 'obj' ||
|
|
||||||
cleanExtension === 'fbx' ||
|
|
||||||
cleanExtension === 'glb'
|
|
||||||
) {
|
|
||||||
return { lowerCase: 'three', upperCase: '3D' };
|
|
||||||
}
|
|
||||||
return { lowerCase: 'file', upperCase: 'File' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件大小
|
|
||||||
export function getSize(size: number) {
|
|
||||||
let retSize = size;
|
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
|
|
||||||
let unitIndex = 0;
|
|
||||||
|
|
||||||
while (retSize >= 1024 && unitIndex < units.length - 1) {
|
|
||||||
retSize /= 1024;
|
|
||||||
unitIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${retSize.toFixed(0)} ${units[unitIndex]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过文件流,生成图片预览
|
|
||||||
// Follow code is copy from `antd/components/upload/utils.ts`:
|
|
||||||
export function isImageFileType(type: string): boolean {
|
|
||||||
return type.indexOf('image/') === 0;
|
|
||||||
}
|
|
||||||
const MEASURE_SIZE = 200;
|
|
||||||
export function previewImage(file: File | Blob): Promise<string> {
|
|
||||||
return new Promise<string>(resolve => {
|
|
||||||
if (!file || !file.type || !isImageFileType(file.type)) {
|
|
||||||
resolve('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
const { width, height } = img;
|
|
||||||
|
|
||||||
const ratio = width / height;
|
|
||||||
const MEASURE_SIZE_WIDTH =
|
|
||||||
ratio > 1 ? MEASURE_SIZE : MEASURE_SIZE * ratio;
|
|
||||||
const MEASURE_SIZE_HEIGHT =
|
|
||||||
ratio > 1 ? MEASURE_SIZE / ratio : MEASURE_SIZE;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = MEASURE_SIZE_WIDTH;
|
|
||||||
canvas.height = MEASURE_SIZE_HEIGHT;
|
|
||||||
canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE_WIDTH}px; height: ${MEASURE_SIZE_HEIGHT}px; z-index: 9999; display: none;`;
|
|
||||||
document.body.appendChild<HTMLCanvasElement>(canvas);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
ctx!.drawImage(img, 0, 0, MEASURE_SIZE_WIDTH, MEASURE_SIZE_HEIGHT);
|
|
||||||
const dataURL = canvas.toDataURL();
|
|
||||||
document.body.removeChild(canvas);
|
|
||||||
window.URL.revokeObjectURL(img.src);
|
|
||||||
resolve(dataURL);
|
|
||||||
};
|
|
||||||
img.crossOrigin = 'anonymous';
|
|
||||||
if (file.type.startsWith('image/svg+xml')) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
if (reader.result && typeof reader.result === 'string') {
|
|
||||||
img.src = reader.result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
else if (file.type.startsWith('image/gif')) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
if (reader.result) {
|
|
||||||
resolve(reader.result as string);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
img.src = window.URL.createObjectURL(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FileCard 组件相关 结束 */
|
|
||||||
|
|
||||||
/* Typewrite 组件相关 开始 */
|
|
||||||
/**
|
|
||||||
* 对代码块添加行号
|
|
||||||
*
|
|
||||||
* @param code 代码html内容
|
|
||||||
* @returns string
|
|
||||||
*/
|
|
||||||
export function generateCodeRowNumber(code: string, source: string) {
|
|
||||||
if (!code) {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = source.split('\n');
|
|
||||||
// 行号html代码拼接列表
|
|
||||||
const rowNumberList = ['<span rn-wrapper aria-hidden="true">'];
|
|
||||||
list.forEach(() => {
|
|
||||||
rowNumberList.push('<span></span>');
|
|
||||||
});
|
|
||||||
rowNumberList.push('</span>');
|
|
||||||
return `<span class="${prefix}-code-block">${code}</span>${rowNumberList.join('')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缩放、拖拽mermaid模块
|
|
||||||
*/
|
|
||||||
export const zoomMermaid = (() => {
|
|
||||||
const addEvent = (container: HTMLElement | null) => {
|
|
||||||
if (!container) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
const content = container.firstChild as HTMLElement;
|
|
||||||
|
|
||||||
let scale = 1;
|
|
||||||
let posX = 0;
|
|
||||||
let posY = 0;
|
|
||||||
let isDragging = false;
|
|
||||||
let startX: number, startY: number;
|
|
||||||
let initialDistance: number;
|
|
||||||
let initialScale = 1;
|
|
||||||
|
|
||||||
const updateTransform = () => {
|
|
||||||
content.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理拖拽和单指移动
|
|
||||||
const onTouchStart = (event: TouchEvent) => {
|
|
||||||
if (event.touches.length === 1) {
|
|
||||||
isDragging = true;
|
|
||||||
startX = event.touches[0].clientX - posX;
|
|
||||||
startY = event.touches[0].clientY - posY;
|
|
||||||
}
|
|
||||||
else if (event.touches.length === 2) {
|
|
||||||
initialDistance = Math.hypot(
|
|
||||||
event.touches[0].clientX - event.touches[1].clientX,
|
|
||||||
event.touches[0].clientY - event.touches[1].clientY
|
|
||||||
);
|
|
||||||
initialScale = scale;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTouchMove = (event: TouchEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (isDragging && event.touches.length === 1) {
|
|
||||||
posX = event.touches[0].clientX - startX;
|
|
||||||
posY = event.touches[0].clientY - startY;
|
|
||||||
updateTransform();
|
|
||||||
}
|
|
||||||
else if (event.touches.length === 2) {
|
|
||||||
const newDistance = Math.hypot(
|
|
||||||
event.touches[0].clientX - event.touches[1].clientX,
|
|
||||||
event.touches[0].clientY - event.touches[1].clientY
|
|
||||||
);
|
|
||||||
const scaleChange = newDistance / initialDistance;
|
|
||||||
const previousScale = scale;
|
|
||||||
scale = initialScale * (1 + (scaleChange - 1)); // 调整缩放速度
|
|
||||||
|
|
||||||
// 计算双指中心点
|
|
||||||
const centerX =
|
|
||||||
(event.touches[0].clientX + event.touches[1].clientX) / 2;
|
|
||||||
const centerY =
|
|
||||||
(event.touches[0].clientY + event.touches[1].clientY) / 2;
|
|
||||||
|
|
||||||
// 获取内容区域的边界
|
|
||||||
const rect = content.getBoundingClientRect();
|
|
||||||
// 计算相对位置
|
|
||||||
const relativeX = (centerX - rect.left) / previousScale;
|
|
||||||
const relativeY = (centerY - rect.top) / previousScale;
|
|
||||||
|
|
||||||
// 调整 posX 和 posY 使得缩放发生在双指中心
|
|
||||||
posX -= relativeX * (scale - previousScale);
|
|
||||||
posY -= relativeY * (scale - previousScale);
|
|
||||||
|
|
||||||
updateTransform();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTouchEnd = () => {
|
|
||||||
isDragging = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// PC 端缩放功能
|
|
||||||
const onWheel = (event: WheelEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const scaleAmount = 0.02; // 缩放速度
|
|
||||||
const previousScale = scale;
|
|
||||||
|
|
||||||
if (event.deltaY < 0) {
|
|
||||||
scale += scaleAmount;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
scale = Math.max(0.1, scale - scaleAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算鼠标相对于内容的位置
|
|
||||||
const rect = content.getBoundingClientRect();
|
|
||||||
const mouseX = event.clientX - rect.left;
|
|
||||||
const mouseY = event.clientY - rect.top;
|
|
||||||
|
|
||||||
// 调整 posX 和 posY,以使缩放中心为鼠标位置
|
|
||||||
posX -= (mouseX / previousScale) * (scale - previousScale);
|
|
||||||
posY -= (mouseY / previousScale) * (scale - previousScale);
|
|
||||||
|
|
||||||
updateTransform();
|
|
||||||
};
|
|
||||||
|
|
||||||
// PC 端拖拽功能
|
|
||||||
const onMouseDown = (event: MouseEvent) => {
|
|
||||||
isDragging = true;
|
|
||||||
startX = event.clientX - posX;
|
|
||||||
startY = event.clientY - posY;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseMove = (event: MouseEvent) => {
|
|
||||||
if (isDragging) {
|
|
||||||
posX = event.clientX - startX;
|
|
||||||
posY = event.clientY - startY;
|
|
||||||
updateTransform();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
isDragging = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseLeave = () => {
|
|
||||||
isDragging = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
container.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
||||||
container.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
||||||
container.addEventListener('touchend', onTouchEnd);
|
|
||||||
container.addEventListener('wheel', onWheel, { passive: false });
|
|
||||||
container.addEventListener('mousedown', onMouseDown);
|
|
||||||
container.addEventListener('mousemove', onMouseMove);
|
|
||||||
container.addEventListener('mouseup', onMouseUp);
|
|
||||||
container.addEventListener('mouseleave', onMouseLeave);
|
|
||||||
|
|
||||||
// 返回一个函数用于注销所有事件
|
|
||||||
return () => {
|
|
||||||
container.removeEventListener('touchstart', onTouchStart);
|
|
||||||
container.removeEventListener('touchmove', onTouchMove);
|
|
||||||
container.removeEventListener('touchend', onTouchEnd);
|
|
||||||
container.removeEventListener('wheel', onWheel);
|
|
||||||
container.removeEventListener('mousedown', onMouseDown);
|
|
||||||
container.removeEventListener('mousemove', onMouseMove);
|
|
||||||
container.removeEventListener('mouseup', onMouseUp);
|
|
||||||
container.removeEventListener('mouseleave', onMouseLeave);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handler = (
|
|
||||||
containers: NodeListOf<HTMLElement>,
|
|
||||||
options: {
|
|
||||||
// customIcon: CustomIconCustomIcon
|
|
||||||
customIcon: any;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
console.log('handler', containers);
|
|
||||||
console.log('options', options);
|
|
||||||
console.log('addEvent', addEvent);
|
|
||||||
|
|
||||||
// const removeEventsMap = new Map<
|
|
||||||
// HTMLElement,
|
|
||||||
// { removeEvent?: () => void, removeClick?: () => void }
|
|
||||||
// >()
|
|
||||||
|
|
||||||
// console.log('containers', containers)
|
|
||||||
|
|
||||||
// containers?.forEach((mm) => {
|
|
||||||
// let actionDiv = mm.querySelector(`.${prefix}-mermaid-action`)
|
|
||||||
|
|
||||||
// if (!actionDiv) {
|
|
||||||
// mm.insertAdjacentHTML(
|
|
||||||
// 'beforeend',
|
|
||||||
// `<div class="${prefix}-mermaid-action">${StrIcon('pin-off', options.customIcon)}</div>`,
|
|
||||||
// )
|
|
||||||
|
|
||||||
// actionDiv = mm.querySelector(`.${prefix}-mermaid-action`)!
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const onClick = () => {
|
|
||||||
// const current = removeEventsMap.get(mm)
|
|
||||||
|
|
||||||
// if (current?.removeEvent) {
|
|
||||||
// // 如果有事件,则注销并移除
|
|
||||||
// current.removeEvent()
|
|
||||||
// mm.removeAttribute('data-grab')
|
|
||||||
// removeEventsMap.set(mm, { removeClick: current.removeClick })
|
|
||||||
// actionDiv.innerHTML = StrIcon('pin-off', options.customIcon) // 取消固定
|
|
||||||
// }
|
|
||||||
// else {
|
|
||||||
// // 添加事件并记录
|
|
||||||
// const removeEvent = addEvent(mm)
|
|
||||||
// mm.setAttribute('data-grab', '')
|
|
||||||
// removeEventsMap.set(mm, { removeEvent, removeClick: current?.removeClick })
|
|
||||||
// actionDiv.innerHTML = StrIcon('pin', options.customIcon) // 固定
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 绑定点击事件
|
|
||||||
// actionDiv.addEventListener('click', onClick)
|
|
||||||
|
|
||||||
// // 将 `click` 事件也放入 Map 中,以便未来注销
|
|
||||||
// removeEventsMap.set(mm, {
|
|
||||||
// removeClick: () => actionDiv.removeEventListener('click', onClick),
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // 返回一个函数,用于注销所有事件(包括 click 和 addEvent)
|
|
||||||
// return () => {
|
|
||||||
// removeEventsMap.forEach(({ removeEvent, removeClick }) => {
|
|
||||||
// removeEvent?.()
|
|
||||||
// removeClick?.()
|
|
||||||
// })
|
|
||||||
// removeEventsMap.clear()
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
return handler;
|
|
||||||
})();
|
|
||||||
/* Typewrite 组件相关 结束 */
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { Ref } from 'vue';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
export default function useFileNameParser(name: Ref<string | undefined>) {
|
|
||||||
const namePrefix = computed(() => {
|
|
||||||
const nameStr = name.value || '';
|
|
||||||
const lastDotIndex = nameStr.lastIndexOf('.');
|
|
||||||
return lastDotIndex === -1 ? nameStr : nameStr.slice(0, lastDotIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
const nameSuffix = computed(() => {
|
|
||||||
const nameStr = name.value || '';
|
|
||||||
const lastDotIndex = nameStr.lastIndexOf('.');
|
|
||||||
if (lastDotIndex === -1 && nameStr.length - lastDotIndex > 10) {
|
|
||||||
// 文件名长度超过10个字符 显示.file
|
|
||||||
return '.file';
|
|
||||||
}
|
|
||||||
return lastDotIndex === -1 ? '' : nameStr.slice(lastDotIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
namePrefix,
|
|
||||||
nameSuffix
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// useScrollDetector.ts
|
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
export default (elementRef: Ref<HTMLElement | null | undefined>) => {
|
|
||||||
const hasVertical = ref(false);
|
|
||||||
const hasHorizontal = ref(false);
|
|
||||||
|
|
||||||
const check = () => {
|
|
||||||
const el = elementRef.value;
|
|
||||||
if (!el)
|
|
||||||
return;
|
|
||||||
hasVertical.value = el.scrollHeight > el.clientHeight;
|
|
||||||
hasHorizontal.value = el.scrollWidth > el.clientWidth;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
check();
|
|
||||||
const observer = new ResizeObserver(check);
|
|
||||||
observer.observe(elementRef.value!);
|
|
||||||
onBeforeUnmount(() => observer.disconnect());
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasVertical, // 是否有纵向滚动条
|
|
||||||
hasHorizontal, // 是否有横向滚动条
|
|
||||||
check, // 检查滚动条状态
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
1
Yi.Ai.Vue3/types/components.d.ts
vendored
1
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -70,6 +70,7 @@ declare module 'vue' {
|
|||||||
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
||||||
Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default']
|
Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default']
|
||||||
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
|
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
|
||||||
|
MarkedMarkdown: typeof import('./../src/components/MarkedMarkdown/index.vue')['default']
|
||||||
ModeList: typeof import('./../src/components/modeList/index.vue')['default']
|
ModeList: typeof import('./../src/components/modeList/index.vue')['default']
|
||||||
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
|
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
|
||||||
NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default']
|
NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default']
|
||||||
|
|||||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -7,6 +7,7 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_WEB_BASE_API: string;
|
readonly VITE_WEB_BASE_API: string;
|
||||||
readonly VITE_API_URL: string;
|
readonly VITE_API_URL: string;
|
||||||
readonly VITE_FILE_UPLOAD_API: string;
|
readonly VITE_FILE_UPLOAD_API: string;
|
||||||
|
readonly VITE_BUILD_COMPRESS: string;
|
||||||
readonly VITE_SSO_SEVER_URL: string;
|
readonly VITE_SSO_SEVER_URL: string;
|
||||||
readonly VITE_APP_VERSION: string;
|
readonly VITE_APP_VERSION: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user