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>
143 lines
5.9 KiB
Vue
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>
|