Full 1:1 migration preserving all visuals, animations and behavior: - Nuxt 4 + Vue 3 (script setup, TypeScript strict) with SSG output - Tailwind CSS v4 with @tailwindcss/vite - Ant Design Vue 4 (dark theme, RTL, Persian locale) for design system - Pinia for state management (ui + contact stores) - motion-v (framer-motion port) for all animations - lucide-vue-next for icons - SEO improvements: og:image, canonical fix, real 404 page, JSON-LD services - JS bundle reduced from 302KB to 239KB gzip - Docker/nginx deployment preserved (out/ -> nginx) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
210 lines
8.3 KiB
Vue
210 lines
8.3 KiB
Vue
<script setup lang="ts">
|
||
import { AnimatePresence, motion } from "motion-v";
|
||
import { ArrowLeft, BadgeCheck, PackageCheck, ShieldCheck } from "lucide-vue-next";
|
||
import Reveal from "~/components/ui/Reveal.vue";
|
||
import SectionHead from "~/components/ui/SectionHead.vue";
|
||
|
||
type Product = {
|
||
code: string;
|
||
name: string;
|
||
en: string;
|
||
desc: string;
|
||
img: string;
|
||
specs: string[];
|
||
};
|
||
|
||
const PRODUCTS: Product[] = [
|
||
{
|
||
code: "AP-101",
|
||
name: "کلاه ایمنی",
|
||
en: "SAFETY HELMET",
|
||
img: "/images/products/helmet.jpg",
|
||
desc: "کلاههای ایمنی با بدنه HDPE مقاوم به ضربه و ترتیهبند ضدتعریق؛ با قابلیت نصب هدفون و شیشه محافظ صورت برای محیطهای ساختمانی و صنعتی.",
|
||
specs: ["EN 397", "مقاوم به ضربه ۵۰ ژول", "بدنه HDPE"],
|
||
},
|
||
{
|
||
code: "AP-205",
|
||
name: "دستکش ضدبرش",
|
||
en: "CUT-RESISTANT GLOVE",
|
||
img: "/images/products/gloves.jpg",
|
||
desc: "دستکشهای سطح ۵ ضدبرش با پوشش نیتریل؛ طراحیشده برای کار با ورق فلز، شیشه و ابزار برنده با چسبندگی عالی در محیط خشک و چرب.",
|
||
specs: ["EN 388 — 4X43C", "سطح ۵ ضدبرش", "پوشش نیتریل"],
|
||
},
|
||
{
|
||
code: "AP-310",
|
||
name: "کفش ایمنی",
|
||
en: "SAFETY BOOTS",
|
||
img: "/images/products/shoes.jpg",
|
||
desc: "کفشهای ایمنی با پنجه فولادی و کف مقاوم در برابر سوراخشدگی و روغن؛ راحتی تمامروز برای شیفتهای طولانی در سایت.",
|
||
specs: ["EN ISO 20345", "پنجه فولادی", "کف ضدروغن"],
|
||
},
|
||
{
|
||
code: "AP-412",
|
||
name: "عینک ایمنی",
|
||
en: "SAFETY GOGGLES",
|
||
img: "/images/products/goggles.jpg",
|
||
desc: "عینکهای محافظ ضدخش با پوشش UV400 و تهویه ضدمه؛ سازگار با عینک طبی برای کارهای جوشکاری، سنگزنی و محیطهای شیمیایی.",
|
||
specs: ["EN 166", "عدسی ضدخش", "پوشش UV400"],
|
||
},
|
||
{
|
||
code: "AP-508",
|
||
name: "هارنس ضدسقوط",
|
||
en: "FULL BODY HARNESS",
|
||
img: "/images/products/harness.jpg",
|
||
desc: "هارنس تمامبدنی با دو شیر پیچی و نوارهای دوختهشده با نخ نارنجی سیگنال؛ تستشده برای کار در ارتفاع با ظرفیت ۱۰۰ کیلوگرم.",
|
||
specs: ["EN 361", "ظرفیت ۱۰۰ کیلوگرم", "شیر دوگانه"],
|
||
},
|
||
{
|
||
code: "AP-615",
|
||
name: "لباس کار بازتابنده",
|
||
en: "HI-VIS WORKWEAR",
|
||
img: "/images/products/vest.jpg",
|
||
desc: "جلیقهها و لباسهای کار بازتابنده کلاس ۲ با نوارهای ۳M؛ دیدهشدن کامل پرسنل در شب، مه و محیطهای پرتردد خودرویی.",
|
||
specs: ["EN ISO 20471", "کلاس ۲", "نوار ۳M"],
|
||
},
|
||
];
|
||
|
||
const TRUST = [
|
||
{
|
||
icon: BadgeCheck,
|
||
title: "گارانتی اصالت کالا",
|
||
desc: "تمام محصولات با فاکتور رسمی و کد رهگیری اصالت",
|
||
},
|
||
{
|
||
icon: PackageCheck,
|
||
title: "تست نمونه قبل از خرید",
|
||
desc: "نمونههای نمایشی برای ارزیابی تیم HSE شما",
|
||
},
|
||
{
|
||
icon: ShieldCheck,
|
||
title: "امکان قرارداد سالانه",
|
||
desc: "قیمت ثابت و تأمین مستمر برای سازمانهای بزرگ",
|
||
},
|
||
];
|
||
|
||
const active = ref(0);
|
||
</script>
|
||
|
||
<template>
|
||
<section id="products" aria-labelledby="products-title" class="relative py-24 md:py-32">
|
||
<div class="mx-auto max-w-7xl px-5 md:px-8">
|
||
<div class="flex flex-wrap items-end justify-between gap-6">
|
||
<SectionHead
|
||
heading-id="products-title"
|
||
tag="۰۱ — محصولات"
|
||
desc="هر محصول پیش از عرضه، زیر ذرهبین کنترل کیفیت و تطبیق با استانداردهای جهانی قرار میگیرد. روی هر پنل کلیک کنید."
|
||
>
|
||
<template #title>
|
||
خط اول دفاع،
|
||
<br />
|
||
تجهیزات ماست
|
||
</template>
|
||
</SectionHead>
|
||
<Reveal :delay="0.2" class="hidden md:block">
|
||
<a
|
||
href="#contact"
|
||
class="group inline-flex items-center gap-2 rounded-full border border-line px-6 py-3 text-sm text-muted transition-colors hover:border-accent hover:text-accent"
|
||
>
|
||
دریافت کاتالوگ کامل
|
||
<ArrowLeft class="size-4 transition-transform group-hover:-translate-x-1" />
|
||
</a>
|
||
</Reveal>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mx-auto mt-14 max-w-7xl px-5 md:px-8">
|
||
<div class="flex flex-col gap-2 md:h-[74vh] md:min-h-[580px] md:flex-row">
|
||
<div
|
||
v-for="(p, i) in PRODUCTS"
|
||
:key="p.code"
|
||
data-hover
|
||
:class="`group relative cursor-pointer overflow-hidden rounded-xl border transition-all duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] ${
|
||
active === i
|
||
? 'h-[500px] flex-[3.4] border-accent/60 md:h-auto'
|
||
: 'h-24 flex-[1] border-line hover:border-ink/30 md:h-auto'
|
||
}`"
|
||
@mouseenter="active = i"
|
||
@click="active = i"
|
||
>
|
||
<img
|
||
:src="p.img"
|
||
:alt="p.name"
|
||
loading="lazy"
|
||
decoding="async"
|
||
:class="`absolute inset-0 size-full object-cover transition-all duration-700 ${
|
||
active === i ? 'scale-100 grayscale-0' : 'scale-110 grayscale group-hover:grayscale-0'
|
||
}`"
|
||
/>
|
||
<div
|
||
:class="`absolute inset-0 transition-opacity duration-700 ${
|
||
active === i
|
||
? 'bg-gradient-to-t from-bg via-bg/35 to-bg/5'
|
||
: 'bg-gradient-to-t from-bg/95 via-bg/30 to-transparent'
|
||
}`"
|
||
/>
|
||
|
||
<div class="absolute inset-x-0 top-0 flex items-center justify-between p-5">
|
||
<span class="font-mono text-[10px] tracking-widest text-ink/60">{{ p.code }}</span>
|
||
<span
|
||
:class="`font-mono text-xs transition-colors duration-500 ${
|
||
active === i ? 'text-accent' : 'text-ink/40'
|
||
}`"
|
||
>
|
||
{{ faNum(String(i + 1).padStart(2, "0")) }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="absolute inset-x-0 bottom-0 p-5 md:p-6">
|
||
<h3
|
||
:class="`font-black transition-all duration-500 ${
|
||
active === i ? 'text-2xl' : 'text-base md:text-sm'
|
||
}`"
|
||
>
|
||
{{ p.name }}
|
||
</h3>
|
||
<p class="mt-1 font-mono text-[10px] tracking-[0.2em] text-muted">{{ p.en }}</p>
|
||
<AnimatePresence>
|
||
<motion.div
|
||
v-if="active === i"
|
||
:initial="{ opacity: 0, y: 18 }"
|
||
:animate="{ opacity: 1, y: 0 }"
|
||
:exit="{ opacity: 0, y: 10 }"
|
||
:transition="{ duration: 0.45, delay: 0.12, ease: EASE }"
|
||
>
|
||
<p class="mt-4 max-w-md text-sm leading-8 text-ink/75">{{ p.desc }}</p>
|
||
<div class="mt-4 flex flex-wrap gap-2">
|
||
<span
|
||
v-for="s in p.specs"
|
||
:key="s"
|
||
class="rounded-full border border-accent/40 bg-accent/10 px-3 py-1 font-mono text-[10px] text-accent"
|
||
>
|
||
{{ s }}
|
||
</span>
|
||
</div>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="mx-auto mt-10 grid max-w-7xl gap-px overflow-hidden rounded-xl border border-line bg-line md:grid-cols-3"
|
||
>
|
||
<Reveal v-for="(t, i) in TRUST" :key="t.title" :delay="i * 0.08">
|
||
<div class="flex h-full items-center gap-4 bg-panel/60 p-6">
|
||
<span
|
||
class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent"
|
||
>
|
||
<component :is="t.icon" class="size-6" />
|
||
</span>
|
||
<div>
|
||
<div class="font-bold">{{ t.title }}</div>
|
||
<div class="mt-1 text-xs leading-6 text-muted">{{ t.desc }}</div>
|
||
</div>
|
||
</div>
|
||
</Reveal>
|
||
</div>
|
||
</section>
|
||
</template>
|