This commit is contained in:
xds
2026-03-22 13:26:38 +03:00
parent f98c57a433
commit beb14b4e43
11 changed files with 272 additions and 62 deletions

View File

@@ -1,58 +1,128 @@
<template>
<div>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Калькулятор 3D-печати</h1>
<p class="mt-1 text-sm text-gray-500">Загрузите модель, выберите материал и получите мгновенный расчёт стоимости</p>
</div>
<HeroSection />
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<FileUploader />
<MaterialPicker @open-advisor="showAdvisor = true" />
<PrintSettings />
<div>
<button
@click="store.calculate()"
:disabled="!store.file || !store.materialId || store.loading"
class="btn-primary w-full sm:w-auto"
>
<svg v-if="store.loading" class="mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{{ store.loading ? 'Считаем...' : 'Рассчитать стоимость' }}
</button>
<p v-if="store.error" class="mt-2 text-sm text-red-600">{{ store.error }}</p>
</div>
<!-- Calculator section -->
<section id="calculator" class="mb-12">
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900">Калькулятор стоимости</h2>
<p class="mt-1 text-sm text-gray-500">Загрузите модель, выберите материал и получите мгновенный расчёт</p>
</div>
<div class="lg:col-span-1">
<div class="sticky top-8">
<PriceResult />
<div v-if="!store.result" class="card text-center text-sm text-gray-400">
<svg class="mx-auto mb-3 h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 15.75V18m-7.5-6.75h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25v-.008zm2.25-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H12.75v-.008zm0 2.25h.008v.008H12.75v-.008zm0 2.25h.008v.008H12.75v-.008zm0 2.25h.008v.008H12.75v-.008zm2.25-4.5h.008v.008H15v-.008zm0 2.25h.008v.008H15v-.008zm0 2.25h.008v.008H15v-.008z" />
</svg>
<p>Загрузите модель и выберите материал для расчёта</p>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 space-y-6">
<FileUploader />
<MaterialPicker @open-advisor="showAdvisor = true" />
<PrintSettings />
<div>
<button
@click="store.calculate()"
:disabled="!store.file || !store.materialId || store.loading"
class="btn-primary w-full sm:w-auto"
>
<svg v-if="store.loading" class="mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{{ store.loading ? 'Считаем...' : 'Рассчитать стоимость' }}
</button>
<p v-if="store.error" class="mt-2 text-sm text-red-600">{{ store.error }}</p>
</div>
</div>
<div class="lg:col-span-1">
<div class="sticky top-20">
<PriceResult />
<div v-if="!store.result" class="card text-center text-sm text-gray-400">
<svg class="mx-auto mb-3 h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 15.75V18m-7.5-6.75h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25v-.008zm0 2.25h.008v.008H8.25v-.008zm2.25-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H12.75v-.008zm0 2.25h.008v.008H12.75v-.008zm0 2.25h.008v.008H12.75v-.008zm0 2.25h.008v.008H12.75v-.008zm2.25-4.5h.008v.008H15v-.008zm0 2.25h.008v.008H15v-.008zm0 2.25h.008v.008H15v-.008z" />
</svg>
<p>Загрузите модель и выберите материал для расчёта</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="mb-12">
<h2 class="mb-6 text-2xl font-bold text-gray-900">Как это работает</h2>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div class="card text-center">
<div class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100">
<svg class="h-6 w-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
</div>
<h3 class="mb-1 text-sm font-bold text-gray-900">Загрузите модель</h3>
<p class="text-xs text-gray-500">STL, 3MF или OBJ файл до 50 МБ. Drag-and-drop или выбор файла.</p>
</div>
<div class="card text-center">
<div class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100">
<svg class="h-6 w-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 class="mb-1 text-sm font-bold text-gray-900">Настройте параметры</h3>
<p class="text-xs text-gray-500">Выберите материал, заполнение, высоту слоя. AI поможет с выбором.</p>
</div>
<div class="card text-center">
<div class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100">
<svg class="h-6 w-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
</svg>
</div>
<h3 class="mb-1 text-sm font-bold text-gray-900">Получите цену</h3>
<p class="text-xs text-gray-500">Детальная разбивка стоимости. Оформите заказ в два клика.</p>
</div>
</div>
</section>
<!-- Latest articles -->
<section class="mb-8">
<div class="mb-6 flex items-center justify-between">
<h2 class="text-2xl font-bold text-gray-900">Полезные статьи</h2>
<router-link to="/blog" class="text-sm font-medium text-primary-600 hover:text-primary-700">
Все статьи &rarr;
</router-link>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<router-link
v-for="article in latestArticles"
:key="article.slug"
:to="`/blog/${article.slug}`"
class="card group transition-shadow hover:shadow-md"
>
<span class="text-xs font-medium text-primary-600">{{ article.category }}</span>
<h3 class="mt-1 text-sm font-bold text-gray-900 group-hover:text-primary-600 transition-colors leading-snug">
{{ article.title }}
</h3>
<p class="mt-1.5 text-xs text-gray-500 line-clamp-2">{{ article.description }}</p>
</router-link>
</div>
</section>
<AiAdvisor :open="showAdvisor" @close="showAdvisor = false" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useCalculatorStore } from '../stores/calculator'
import HeroSection from '../components/HeroSection.vue'
import FileUploader from '../components/FileUploader.vue'
import MaterialPicker from '../components/MaterialPicker.vue'
import PrintSettings from '../components/PrintSettings.vue'
import PriceResult from '../components/PriceResult.vue'
import AiAdvisor from '../components/AiAdvisor.vue'
import { articles } from '../data/articles'
const store = useCalculatorStore()
const showAdvisor = ref(false)
const latestArticles = computed(() =>
[...articles].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 3)
)
</script>