Files
filam3d/frontend/src/views/ArticleView.vue
2026-03-23 12:48:44 +03:00

132 lines
5.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>