Files
imen_pishghaman/app/components/sections/Services.vue
T
S. EsmaeiliandClaude Opus 4.6 e31ef5a21f feat: migrate landing from Next.js to Nuxt 4 (Vue 3)
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>
2026-10-06 16:28:05 +03:30

143 lines
5.9 KiB
Vue

<script setup lang="ts">
import { AnimatePresence, motion } from "motion-v";
import { ClipboardCheck, Gauge, GraduationCap, Plus, Truck } from "lucide-vue-next";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
import { serviceSchema } from "#shared/seo/schema";
const SERVICES = [
{
icon: ClipboardCheck,
title: "بازرسی و ارزیابی ریسک",
desc: "تیم مهندسی ما با بازدید از سایت شما، خطرات را شناسایی، اولویت‌بندی و برای هر کدام راهکار اجرایی مشخص با برآورد هزینه ارائه می‌دهد.",
items: [
"ارزیابی HAZOP و HAZID",
"شناسایی و اولویت‌بندی خطرات",
"گزارش مدیریتی با راهکار اجرایی",
],
},
{
icon: GraduationCap,
title: "آموزش تخصصی ایمنی",
desc: "از دوره‌های عمومی تا تربیت مربی؛ آموزش‌های عملی و میدانی که ایمنی را از دفتر کلان‌ها به رفتار روزمره پرسنل تبدیل می‌کند.",
items: [
"دوره‌های عمومی و تخصصی HSE",
"تربیت مربی ایمنی سایت",
"مانور و شبیه‌سازی شرایط اضطراری",
],
},
{
icon: Gauge,
title: "تست و کالیبراسیون تجهیزات",
desc: "تجهیزات ایمنی فقط وقتی قابل اعتمادند که تست شده باشند. تست دوره‌ای، سرویس و صدور گواهی معتبر را به تیم تخصصی ما بسپارید.",
items: [
"تست هارنس و لاین نجات",
"کالیبراسیون آشکارسازهای گاز",
"سرویس دوره‌ای و صدور گواهی",
],
},
{
icon: Truck,
title: "تأمین و لجستیک سازمانی",
desc: "با قرارداد سالانه، قیمت‌گذاری شفاف و انبار محلی در سایت شما، نگرانی کمبود تجهیزات را از فهرست دغدغه‌هایتان حذف کنید.",
items: [
"قرارداد سالانه با قیمت ثابت",
"انبار محلی در سایت شما",
"تحویل سراسری در ۴۸ ساعت",
],
},
];
useJsonLd(
SERVICES.map((s) => serviceSchema({ name: s.title, description: s.desc })),
"services",
);
const open = ref(0);
</script>
<template>
<section id="services" aria-labelledby="services-title" class="border-t border-line py-24 md:py-32">
<div class="mx-auto max-w-7xl px-5 md:px-8">
<SectionHead
heading-id="services-title"
tag="۰۳ — خدمات"
desc="ما در آریا ایمن، تجهیزات را تحویل می‌دهیم و کنار شما می‌مانیم؛ از نخستین ارزیابی ریسک تا آخرین تست دوره‌ای."
>
<template #title>
فراتر از تأمین؛
<br />
همراهی کامل ایمنی
</template>
</SectionHead>
<div class="mt-16">
<Reveal v-for="(s, i) in SERVICES" :key="s.title" :delay="i * 0.05">
<div :class="`border-t border-line ${i === SERVICES.length - 1 ? 'border-b' : ''}`">
<button
class="group flex w-full items-center gap-5 py-7 text-start md:gap-10 md:py-9"
@click="open = open === i ? -1 : i"
>
<span
:class="`font-mono text-sm transition-colors duration-300 md:text-base ${
open === i ? 'text-accent' : 'text-muted'
}`"
>
{{ faNum(String(i + 1).padStart(2, "0")) }}
</span>
<span
:class="`flex-1 text-2xl font-black transition-colors duration-300 md:text-4xl ${
open === i ? 'text-accent' : 'text-ink group-hover:text-accent'
}`"
>
{{ s.title }}
</span>
<span
:class="`hidden size-12 shrink-0 items-center justify-center rounded-full border transition-all duration-500 md:flex ${
open === i
? 'rotate-45 border-accent bg-accent text-bg'
: 'border-line text-muted group-hover:border-accent/50 group-hover:text-accent'
}`"
>
<Plus class="size-5" />
</span>
</button>
<AnimatePresence :initial="false">
<motion.div
v-if="open === i"
:initial="{ height: 0, opacity: 0 }"
:animate="{ height: 'auto', opacity: 1 }"
:exit="{ height: 0, opacity: 0 }"
:transition="{ duration: 0.55, ease: EASE }"
class="overflow-hidden"
>
<div class="grid gap-8 pb-10 md:grid-cols-2 md:ps-16">
<div class="flex items-start gap-4">
<span
class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent"
>
<component :is="s.icon" class="size-6" />
</span>
<p class="text-sm leading-8 text-muted">{{ s.desc }}</p>
</div>
<ul class="space-y-3.5">
<li
v-for="it in s.items"
:key="it"
class="flex items-center gap-3 text-sm text-ink/80"
>
<span class="size-1.5 shrink-0 rounded-full bg-accent" />
{{ it }}
</li>
</ul>
</div>
</motion.div>
</AnimatePresence>
</div>
</Reveal>
</div>
</div>
</section>
</template>