132 lines
5.5 KiB
Vue
132 lines
5.5 KiB
Vue
<template>
|
||
<div v-if="article" class="mx-auto max-w-3xl">
|
||
<router-link to="/blog" class="mb-6 inline-flex items-center text-sm text-gray-500 hover:text-gray-700">
|
||
<svg class="mr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||
</svg>
|
||
Все статьи
|
||
</router-link>
|
||
|
||
<article>
|
||
<header class="mb-8">
|
||
<div class="mb-3 flex items-center gap-3">
|
||
<span class="rounded-full bg-primary-100 px-3 py-1 text-xs font-medium text-primary-700">
|
||
{{ article.category }}
|
||
</span>
|
||
<span class="text-sm text-gray-400">{{ formatDate(article.date) }}</span>
|
||
<span class="text-sm text-gray-400">{{ article.readTime }} мин чтения</span>
|
||
</div>
|
||
<h1 class="text-3xl font-bold text-gray-900 leading-tight">{{ article.title }}</h1>
|
||
<p class="mt-3 text-lg text-gray-500">{{ article.description }}</p>
|
||
</header>
|
||
|
||
<div class="prose prose-gray max-w-none" v-html="renderedContent"></div>
|
||
|
||
<!-- CTA -->
|
||
<div class="mt-12 rounded-xl bg-primary-50 border border-primary-100 p-6 text-center">
|
||
<h3 class="text-lg font-bold text-gray-900 mb-2">Нужна 3D-печать?</h3>
|
||
<p class="text-sm text-gray-600 mb-4">Загрузите модель и получите точный расчёт стоимости за секунды</p>
|
||
<router-link to="/" class="btn-primary">Рассчитать стоимость</router-link>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Related articles -->
|
||
<div v-if="relatedArticles.length" class="mt-12">
|
||
<h3 class="mb-4 text-lg font-bold text-gray-900">Читайте также</h3>
|
||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<router-link
|
||
v-for="related in relatedArticles"
|
||
:key="related.slug"
|
||
:to="`/blog/${related.slug}`"
|
||
class="card group transition-shadow hover:shadow-md"
|
||
>
|
||
<span class="text-xs text-primary-600 font-medium">{{ related.category }}</span>
|
||
<h4 class="mt-1 text-sm font-bold text-gray-900 group-hover:text-primary-600 transition-colors">
|
||
{{ related.title }}
|
||
</h4>
|
||
</router-link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="text-center py-20">
|
||
<p class="text-gray-500">Статья не найдена</p>
|
||
<router-link to="/blog" class="btn-primary mt-4 inline-block">Все статьи</router-link>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, watch } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { getArticleBySlug, articles } from '../data/articles'
|
||
|
||
const route = useRoute()
|
||
|
||
const article = computed(() => getArticleBySlug(route.params.slug))
|
||
|
||
const renderedContent = computed(() => {
|
||
if (!article.value) return ''
|
||
return simpleMarkdown(article.value.content)
|
||
})
|
||
|
||
const relatedArticles = computed(() => {
|
||
if (!article.value) return []
|
||
return articles
|
||
.filter((a) => a.slug !== article.value.slug)
|
||
.filter((a) => a.category === article.value.category)
|
||
.slice(0, 2)
|
||
})
|
||
|
||
watch(() => route.params.slug, () => {
|
||
window.scrollTo(0, 0)
|
||
if (article.value) {
|
||
document.title = `${article.value.title} — Bambu Russia`
|
||
}
|
||
}, { immediate: true })
|
||
|
||
function formatDate(dateStr) {
|
||
return new Date(dateStr).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })
|
||
}
|
||
|
||
function simpleMarkdown(md) {
|
||
let html = md
|
||
.replace(/^### (.+)$/gm, '<h3 class="text-lg font-bold text-gray-900 mt-6 mb-2">$1</h3>')
|
||
.replace(/^## (.+)$/gm, '<h2 class="text-xl font-bold text-gray-900 mt-8 mb-3">$1</h2>')
|
||
.replace(/^\- \[ \] (.+)$/gm, '<div class="flex items-center gap-2 my-1"><input type="checkbox" disabled class="rounded"><span class="text-sm text-gray-700">$1</span></div>')
|
||
.replace(/^\- (.+)$/gm, '<li class="ml-4 text-gray-700">$1</li>')
|
||
.replace(/^(\d+)\. (.+)$/gm, '<li class="ml-4 text-gray-700 list-decimal">$2</li>')
|
||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||
.replace(/`(.+?)`/g, '<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-primary-700">$1</code>')
|
||
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" class="text-primary-600 hover:text-primary-700 underline">$1</a>')
|
||
.replace(/^---$/gm, '<hr class="my-6 border-gray-200">')
|
||
.replace(/^\|(.+)$/gm, (match) => {
|
||
const cells = match.split('|').filter(c => c.trim())
|
||
if (cells.every(c => /^[\s-:]+$/.test(c))) return ''
|
||
const isHeader = match.includes('---')
|
||
const tag = isHeader ? 'th' : 'td'
|
||
const cls = isHeader
|
||
? 'px-3 py-2 text-left text-xs font-semibold text-gray-600 bg-gray-50'
|
||
: 'px-3 py-2 text-sm text-gray-700 border-t border-gray-100'
|
||
const row = cells.map(c => `<${tag} class="${cls}">${c.trim()}</${tag}>`).join('')
|
||
return `<tr>${row}</tr>`
|
||
})
|
||
|
||
// Wrap table rows
|
||
html = html.replace(/((<tr>.*<\/tr>\s*)+)/g, '<div class="overflow-x-auto my-4"><table class="w-full border border-gray-200 rounded-lg overflow-hidden">$1</table></div>')
|
||
|
||
// Wrap list items
|
||
html = html.replace(/((<li class="ml-4 text-gray-700">.*<\/li>\s*)+)/g, '<ul class="my-3 space-y-1">$1</ul>')
|
||
|
||
// Paragraphs for remaining text
|
||
html = html.split('\n').map(line => {
|
||
const trimmed = line.trim()
|
||
if (!trimmed) return ''
|
||
if (trimmed.startsWith('<')) return line
|
||
return `<p class="my-3 text-gray-700 leading-relaxed">${trimmed}</p>`
|
||
}).join('\n')
|
||
|
||
return html
|
||
}
|
||
</script>
|