This commit is contained in:
xds
2026-03-22 13:26:18 +03:00
parent 28a5d51389
commit f98c57a433
20 changed files with 2949 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
<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} — Filam3D`
}
}, { 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>