/
githubmirror
/
ChatDev
Обзор
Документация
Войти
/
githubmirror
/
ChatDev
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
frontend/src/pages/TutorialView.vue
414 строк
11 KB
YunLong
feat: Add comprehensive i18n support and fix WorkflowEdge crash bug
01 апр 2026, 13:54
01 апр 2026, 13:54
37aaf31
Код
Авторство
О чём код?
<script setup> import { ref, onMounted, nextTick, watch } from 'vue' import { useRoute } from 'vue-router' import MarkdownIt from 'markdown-it' import markdownItAnchor from 'markdown-it-anchor' import { useI18n } from 'vue-i18n' const route = useRoute() const { locale, t } = useI18n() const renderedContent = ref('') const markdownBody = ref(null) const md = new MarkdownIt({ html: true, linkify: true, typographer: true }) md.use(markdownItAnchor, { permalink: markdownItAnchor.permalink.ariaHidden({ placement: 'before', symbol: '#', space: true, class: 'header-anchor' }), slugify: s => s .trim() .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5\s-]/g, '') .replace(/\s+/g, '-') }) const getTutorialFile = () => (locale.value === 'en' ? '/tutorial-en.md' : '/tutorial-zh.md') const scrollToHash = () => { // Wait for DOM to update, then scroll to hash if present nextTick(() => { if (route.hash) { // Remove the '#' from the hash const targetId = route.hash.slice(1) const targetElement = document.getElementById(targetId) if (targetElement) { setTimeout(() => { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 100) } } }) } const addCopyButtons = () => { nextTick(() => { const container = markdownBody.value if (!container) return const blocks = container.querySelectorAll('pre') blocks.forEach((block) => { if (block.querySelector('.copy-code-btn')) return const button = document.createElement('button') button.type = 'button' button.className = 'copy-code-btn' button.textContent = t('tutorial.copy') button.addEventListener('click', async () => { const code = block.querySelector('code') const text = code ? code.innerText : block.innerText try { await navigator.clipboard.writeText(text) button.textContent = t('tutorial.copied') setTimeout(() => { button.textContent = t('tutorial.copy') }, 1200) } catch (error) { console.error(t('tutorial.failed_to_copy'), error) } }) block.classList.add('has-copy-button') block.appendChild(button) }) }) } const loadTutorial = async () => { try { const response = await fetch(getTutorialFile()) if (response.ok) { let text = await response.text() // Fix media paths to point to /media/ (absolute path from public root) // For markdown images:  ->  text = text.replace(/\]\(media\//g, '](/media/') // For HTML image tags: src="media/file.gif" -> src="/media/file.gif" text = text.replace(/src="media\//g, 'src="/media/') renderedContent.value = md.render(text) addCopyButtons() scrollToHash() } else { console.error(t('tutorial.failed_to_fetch')) } } catch (error) { console.error(t('tutorial.failed_to_load'), error) } } const switchLang = (lang) => { if (locale.value !== lang) { locale.value = lang } } watch(locale, () => { loadTutorial() }) onMounted(() => { loadTutorial() }) </script> <template> <div class="tutorial-view"> <div class="lang-switch"> <button :class="{ active: locale === 'zh' }" @click="switchLang('zh')">{{ $t('tutorial.zh') }}</button> <button :class="{ active: locale === 'en' }" @click="switchLang('en')">{{ $t('tutorial.en') }}</button> </div> <div ref="markdownBody" class="markdown-body" v-html="renderedContent"></div> </div> </template> <style scoped> .tutorial-view { padding: 40px; width: 100%; max-width: 100%; margin: 0 auto; height: calc(100vh - 55px); /* Adjust the height of the .sidebar */ background: linear-gradient(135deg, #232526 0%, #1e1e1e 100%); overflow-y: auto; overflow-x: hidden; box-sizing: border-box; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* Glowing border and card shadow */ border-radius: 18px; box-shadow: 0 4px 32px 0 rgba(0, 255, 255, 0.08), 0 0 0 2px #00eaff33; border: 1.5px solid #00eaff33; transition: box-shadow 0.3s, height 0.3s cubic-bezier(0.4, 0, 0.2, 1); scroll-behavior: smooth; } :global(body.nav-hidden .tutorial-view) { height: 100vh; } .lang-switch { position: absolute; top: 80px; /* 55px nav + 25px spacing */ right: 48px; z-index: 10; display: flex; gap: 12px; transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1); } :global(body.nav-hidden .lang-switch) { top: 25px; /* Adjust to 25px spacing from top of screen when nav is hidden */ } .lang-switch button { background: linear-gradient(90deg, #232526 60%, #00eaff22 100%); color: #00eaff; border: 1.5px solid #00eaff33; border-radius: 8px; padding: 6px 18px; font-size: 1em; font-family: inherit; cursor: pointer; transition: background 0.2s, color 0.2s, border 0.2s, box-shadow 0.2s; box-shadow: 0 2px 8px #00eaff11; } .lang-switch button.active, .lang-switch button:hover { background: linear-gradient(90deg, #00eaff 0%, #232526 100%); color: #fff; border: 1.5px solid #00eaff; box-shadow: 0 0 12px #00eaff44; } :deep(.markdown-body) { max-width: 980px; margin: 0 auto; color: #e0e0e0; font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'monospace', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; line-height: 1.7; letter-spacing: 0.01em; background: rgba(30,34,40,0.85); border-radius: 14px; box-shadow: 0 2px 16px 0 rgba(0,255,255,0.04); padding: 32px 36px 32px 36px; border: 1px solid #00eaff22; backdrop-filter: blur(2px); } :deep(.markdown-body h1), :deep(.markdown-body h2), :deep(.markdown-body h3), :deep(.markdown-body h4), :deep(.markdown-body h5), :deep(.markdown-body h6) { margin-top: 24px; margin-bottom: 16px; font-weight: 700; line-height: 1.25; color: #00eaff; text-shadow: 0 0 8px #00eaff44; letter-spacing: 0.02em; scroll-margin-top: 20px; /* Offset for hash scroll target */ transition: background 0.3s ease; } /* Highlight targeted heading */ :deep(.markdown-body h1:target), :deep(.markdown-body h2:target), :deep(.markdown-body h3:target), :deep(.markdown-body h4:target), :deep(.markdown-body h5:target), :deep(.markdown-body h6:target) { background: rgba(0, 234, 255, 0.15); padding: 8px 12px; margin-left: -12px; margin-right: -12px; border-radius: 6px; box-shadow: 0 0 16px rgba(0, 234, 255, 0.3); } :deep(.markdown-body h1) { font-size: 2.2em; border-bottom: 1px solid #00eaff33; padding-bottom: 0.3em; } :deep(.markdown-body h2) { font-size: 1.6em; border-bottom: 1px solid #00eaff22; padding-bottom: 0.3em; } :deep(.markdown-body h3) { font-size: 1.3em; } :deep(.markdown-body p) { margin-top: 0; margin-bottom: 16px; color: #b8eaff; font-size: 1.08em; } :deep(.markdown-body a) { color: #00eaff; text-decoration: none; border-bottom: 1px dashed #00eaff99; transition: color 0.2s, border-bottom 0.2s; } :deep(.markdown-body a:hover) { color: #fff; border-bottom: 1px solid #00eaff; text-shadow: 0 0 6px #00eaff99; } :deep(.markdown-body img) { width: 100%; max-width: 100%; box-sizing: border-box; background-color: transparent; border-radius: 10px; border: 1.5px solid #00eaff33; box-shadow: 0 2px 16px 0 #00eaff22; margin: 16px 0; transition: box-shadow 0.2s; } :deep(.markdown-body img:hover) { box-shadow: 0 0 24px 2px #00eaff88; } :deep(.markdown-body video) { max-width: 100%; border-radius: 10px; border: 1.5px solid #00eaff33; margin-bottom: 16px; box-shadow: 0 2px 16px 0 #00eaff22; } :deep(.markdown-body code) { padding: 0.2em 0.5em; margin: 0; font-size: 90%; background: linear-gradient(90deg, #232526 60%, #00eaff22 100%); border-radius: 6px; color: #00eaff; font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'monospace'; border: 1px solid #00eaff33; box-shadow: 0 0 8px #00eaff22; } :deep(.markdown-body pre) { padding: 18px 20px; overflow: auto; font-size: 95%; line-height: 1.55; background: linear-gradient(90deg, #161b22 80%, #00eaff11 100%); border-radius: 10px; margin-bottom: 18px; border: 1.5px solid #00eaff33; box-shadow: 0 0 16px #00eaff11; position: relative; white-space: pre-wrap; overflow-wrap: anywhere; } :deep(.markdown-body pre code) { display: inline; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background: transparent; border: 0; color: #00eaff; white-space: pre-wrap; overflow-wrap: anywhere; } :deep(.markdown-body pre.has-copy-button) { padding-top: 44px; } :deep(.markdown-body .copy-code-btn) { position: absolute; top: 10px; right: 10px; background: linear-gradient(90deg, #232526 60%, #00eaff22 100%); color: #00eaff; border: 1px solid #00eaff44; border-radius: 6px; padding: 4px 10px; font-size: 0.85em; cursor: pointer; transition: background 0.2s, color 0.2s, border 0.2s, box-shadow 0.2s; box-shadow: 0 2px 8px #00eaff11; } :deep(.markdown-body .copy-code-btn:hover) { background: linear-gradient(90deg, #00eaff 0%, #232526 100%); color: #fff; border: 1px solid #00eaff; box-shadow: 0 0 12px #00eaff44; } :deep(.markdown-body blockquote) { padding: 0 1em; color: #8bffe6; border-left: 0.25em solid #00eaff99; margin: 0 0 16px 0; background: rgba(0,234,255,0.04); border-radius: 6px; } :deep(.markdown-body hr) { height: 0.25em; padding: 0; margin: 24px 0; background: linear-gradient(90deg, #00eaff33 0%, #232526 100%); border: 0; border-radius: 2px; } :deep(.markdown-body table) { border-spacing: 0; border-collapse: collapse; margin-top: 0; margin-bottom: 16px; width: 100%; overflow: auto; background: rgba(0,234,255,0.02); border-radius: 8px; } :deep(.markdown-body table th), :deep(.markdown-body table td) { padding: 8px 15px; border: 1px solid #00eaff22; } :deep(.markdown-body table th) { font-weight: 700; background: rgba(0,234,255,0.08); color: #00eaff; } :deep(.markdown-body table tr) { background: rgba(30,34,40,0.7); border-top: 1px solid #00eaff11; } :deep(.markdown-body table tr:nth-child(2n)) { background: rgba(0,234,255,0.03); } /* Custom Scrollbar */ .tutorial-view::-webkit-scrollbar { width: 10px; background: #232526; } .tutorial-view::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #00eaff 0%, #232526 100%); border-radius: 8px; } :deep(.markdown-body::-webkit-scrollbar) { height: 8px; background: #232526; } :deep(.markdown-body::-webkit-scrollbar-thumb) { background: linear-gradient(90deg, #00eaff 0%, #232526 100%); border-radius: 8px; } </style>