| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- <script setup>
- import { Icon } from "@iconify/vue";
- import { computed, onBeforeUnmount, onMounted, ref } from "vue";
- import PageHero from "../components/PageHero.vue";
- import Reveal from "../components/motion/Reveal.vue";
- import bannerUrl from "../assets/images/banner.jpg?url";
- import bgUrl from "../assets/images/bg.jpg?url";
- const activeTab = ref("equipment");
- const expanded = ref({
- equipment: false,
- qhse: false,
- injection: false,
- });
- const prefersReducedMotion = () =>
- typeof window !== "undefined" &&
- window.matchMedia &&
- window.matchMedia("(prefers-reduced-motion: reduce)").matches;
- const categories = [
- {
- key: "equipment",
- title: "设备管理",
- lead: "覆盖台账、巡检、维保、预测性维护到告警处置的全生命周期闭环,让设备运行更稳定、维护更可控。",
- icon: "lucide:cpu",
- accent: "rgba(37, 99, 235, 0.9)",
- cover: bannerUrl,
- cases: [
- {
- title: "设备台账与参数标准化",
- desc: "统一资产主数据、参数口径与编码规则,沉淀可复用的设备数据底座。",
- tags: ["台账治理", "主数据", "标准化"],
- metrics: ["资产一致率 ≥99%", "新增设备上线 T+1", "编码规则统一"],
- },
- ],
- },
- {
- key: "qhse",
- title: "QHSE",
- lead: "将质量、健康、安全、环境融入业务流程:从风险识别到整改闭环,让合规与效率同时提升。",
- icon: "lucide:shield-check",
- accent: "rgba(14, 116, 144, 0.92)",
- cover: bgUrl,
- cases: [
- {
- title: "隐患排查治理与整改闭环",
- desc: "隐患登记、分级分责、整改验收一体化,形成闭环追踪与审计留痕。",
- tags: ["隐患", "整改", "闭环"],
- metrics: ["闭环率 ≥98%", "复发率下降", "审计留痕完整"],
- },
- ],
- },
- {
- key: "injection",
- title: "智能注气",
- lead: "围绕注气作业计划、过程监控与效果评估,联动工艺参数与产量数据,实现可控、可追溯、可优化。",
- icon: "lucide:wind",
- accent: "rgba(2, 132, 199, 0.92)",
- cover: bannerUrl,
- cases: [
- {
- title: "注气计划编排与批量下发",
- desc: "按井/站点编排注气计划,批量下发到现场执行端,确保节奏一致。",
- tags: ["计划", "下发", "协同"],
- metrics: ["计划变更可追溯", "下发效率提升", "执行偏差可视化"],
- },
- ],
- },
- ];
- const tabItems = computed(() =>
- categories.map((c) => ({
- ...c,
- count: c.cases.length,
- })),
- );
- const maxPreview = 6;
- const visibleCases = (cat) =>
- expanded.value[cat.key] ? cat.cases : cat.cases.slice(0, maxPreview);
- const toggleExpand = (key) => {
- expanded.value = { ...expanded.value, [key]: !expanded.value[key] };
- };
- const scrollToCategory = (key) => {
- const el = document.getElementById(`case-${key}`);
- if (!el) return;
- activeTab.value = key;
- el.scrollIntoView({
- behavior: prefersReducedMotion() ? "auto" : "smooth",
- block: "start",
- });
- };
- let io = null;
- onMounted(() => {
- if (typeof window === "undefined") return;
- if (!("IntersectionObserver" in window)) return;
- const headerHRaw = getComputedStyle(document.documentElement)
- .getPropertyValue("--header-h")
- .trim();
- const headerH = Number.parseInt(headerHRaw, 10);
- const topOffset = Number.isFinite(headerH) ? headerH : 72;
- const sections = categories
- .map((c) => document.getElementById(`case-${c.key}`))
- .filter(Boolean);
- io = new IntersectionObserver(
- (entries) => {
- const visible = entries
- .filter((e) => e.isIntersecting)
- .sort(
- (a, b) =>
- (a.boundingClientRect.top ?? 0) - (b.boundingClientRect.top ?? 0),
- )[0];
- if (!visible) return;
- const id = visible.target.getAttribute("id") || "";
- const key = id.replace("case-", "");
- if (key) activeTab.value = key;
- },
- {
- threshold: 0.22,
- rootMargin: `-${topOffset + 10}px 0px -55% 0px`,
- },
- );
- for (const s of sections) io.observe(s);
- });
- onBeforeUnmount(() => {
- if (io) io.disconnect();
- });
- </script>
- <template>
- <div class="casesView">
- <PageHero
- kicker="客户案例"
- title="案例分为三大类:设备管理、QHSE、智能注气"
- subtitle="每个分类沉淀了多项可复用的落地案例与最佳实践,覆盖从方案设计到运营闭环的关键环节。"
- >
- <template #actions>
- <RouterLink
- class="btn btn-primary"
- style="border-radius: 0"
- to="/contact"
- >留言咨询</RouterLink
- >
- </template>
- </PageHero>
- <nav class="tabBar" aria-label="案例分类导航">
- <button
- v-for="t in tabItems"
- :key="t.key"
- type="button"
- class="tabBtn"
- :class="{ 'is-active': activeTab === t.key }"
- @click="scrollToCategory(t.key)"
- >
- <Icon :icon="t.icon" class="tabIcon" />
- <span class="tabText">{{ t.title }}</span>
- <span class="tabCount">{{ t.count }}</span>
- </button>
- </nav>
- <section class="section">
- <div class="container">
- <div class="stack">
- <section
- v-for="(cat, catIdx) in categories"
- :key="cat.key"
- class="caseSection"
- :id="`case-${cat.key}`"
- :style="{ '--accent': cat.accent }"
- >
- <Reveal as="header" class="caseHead" :delay="40 + catIdx * 80">
- <div class="caseHeadTop">
- <span class="caseBadge">分类</span>
- <div class="caseHeadTitle">
- <Icon :icon="cat.icon" class="caseHeadIcon" />
- <h2 class="h2">{{ cat.title }}</h2>
- </div>
- </div>
- <p class="muted caseLead">{{ cat.lead }}</p>
- <div class="caseHeadActions">
- <RouterLink
- class="btn btn-ghost"
- style="border-radius: 0"
- to="/contact"
- >对接同类项目</RouterLink
- >
- </div>
- </Reveal>
- <div class="caseGrid">
- <Reveal
- v-for="(c, idx) in visibleCases(cat)"
- :key="c.title"
- as="article"
- class="card caseCard hover-lift"
- :delay="60 + idx * 60"
- >
- <div class="caseCardTop">
- <div class="caseCardLine" aria-hidden="true"></div>
- <div class="caseCardTitle">{{ c.title }}</div>
- <div class="muted caseCardDesc">{{ c.desc }}</div>
- </div>
- <div class="caseMeta">
- <div class="caseTags" aria-label="标签">
- <span v-for="t in c.tags" :key="t" class="tag">{{
- t
- }}</span>
- </div>
- <div class="caseMetrics" aria-label="指标">
- <span v-for="m in c.metrics" :key="m" class="metric">{{
- m
- }}</span>
- </div>
- </div>
- </Reveal>
- </div>
- <div class="caseMore">
- <button
- v-if="cat.cases.length > maxPreview"
- type="button"
- class="btn btn-link moreBtn"
- @click="toggleExpand(cat.key)"
- >
- {{
- expanded[cat.key]
- ? "收起"
- : `展开更多(${cat.cases.length - maxPreview})`
- }}
- </button>
- </div>
- </section>
- </div>
- </div>
- </section>
- </div>
- </template>
- <style scoped>
- .casesView {
- --tab-bg: rgba(255, 255, 255, 0.92);
- }
- .tabBar {
- position: sticky;
- top: var(--header-h);
- z-index: 20;
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- background: #eef5f8;
- height: 76px;
- margin: 10px 0 18px;
- border: 0;
- border-radius: 0;
- }
- .tabBtn {
- height: 76px;
- border: 0;
- background: #eef5f8;
- padding: 0 18px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- color: var(--slate-700);
- cursor: pointer;
- transition:
- transform 160ms ease,
- box-shadow 160ms ease,
- color 160ms ease,
- background-color 160ms ease;
- }
- .tabBtn.is-active {
- color: var(--brand-700);
- background-color: #fff;
- }
- .tabBtn:focus-visible {
- outline: none;
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.22);
- }
- .tabIcon {
- width: 18px;
- height: 18px;
- }
- .tabText {
- display: inline-block;
- white-space: nowrap;
- font-weight: 900;
- letter-spacing: -0.01em;
- padding-bottom: 10px;
- background-image: linear-gradient(currentColor, currentColor);
- background-repeat: no-repeat;
- background-position: 0 100%;
- background-size: 0 2px;
- transition: background-size 160ms ease;
- }
- .tabBtn.is-active .tabText {
- background-size: 100% 2px;
- }
- .tabCount {
- font-size: 12px;
- font-weight: 900;
- padding: 4px 8px;
- border-radius: 999px;
- border: 1px solid rgba(15, 23, 42, 0.12);
- background: rgba(255, 255, 255, 0.72);
- color: rgba(2, 6, 23, 0.72);
- }
- .stack {
- display: grid;
- gap: 26px;
- }
- .caseSection {
- scroll-margin-top: calc(var(--header-h) + 88px);
- padding: 18px 0 10px;
- }
- .caseHead {
- padding: 18px 4px 14px;
- display: grid;
- gap: 10px;
- }
- .caseHeadTop {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
- flex-wrap: wrap;
- }
- .caseBadge {
- font-size: 12px;
- font-weight: 900;
- letter-spacing: 0.12em;
- color: rgba(2, 6, 23, 0.62);
- background: rgba(15, 23, 42, 0.04);
- border: 1px solid rgba(15, 23, 42, 0.08);
- padding: 6px 10px;
- border-radius: 999px;
- flex: 0 0 auto;
- }
- .caseHeadTitle {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- }
- .caseHeadIcon {
- width: 20px;
- height: 20px;
- color: var(--accent);
- }
- .caseLead {
- max-width: 92ch;
- }
- .caseHeadActions {
- margin-top: 2px;
- }
- .caseGrid {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 18px;
- margin-top: 10px;
- }
- .caseCard {
- border: 1px solid var(--border);
- border-top: 0;
- box-shadow: 0 16px 42px rgba(2, 6, 23, 0.08);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), #fff);
- }
- .caseCardTop {
- padding: 18px 18px 10px;
- }
- .caseCardLine {
- height: 2px;
- width: 100%;
- background: linear-gradient(90deg, var(--accent), rgba(2, 6, 23, 0));
- margin-bottom: 12px;
- }
- .caseCardTitle {
- font-weight: 900;
- letter-spacing: -0.02em;
- line-height: 1.25;
- }
- .caseCardDesc {
- margin-top: 8px;
- line-height: 1.75;
- }
- .caseMeta {
- padding: 8px 18px 18px;
- display: grid;
- gap: 12px;
- }
- .caseTags {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .tag {
- display: inline-flex;
- align-items: center;
- padding: 6px 10px;
- border-radius: 999px;
- background: rgba(15, 23, 42, 0.03);
- border: 1px solid var(--border);
- font-weight: 700;
- color: var(--slate-700);
- font-size: 12px;
- }
- .caseMetrics {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- }
- .metric {
- display: inline-flex;
- align-items: center;
- padding: 6px 10px;
- background: rgba(15, 23, 42, 0.03);
- border: 1px solid var(--border);
- border-left: 2px solid var(--accent);
- font-weight: 800;
- color: rgba(2, 6, 23, 0.78);
- font-size: 12px;
- }
- .caseMore {
- margin-top: 10px;
- display: flex;
- justify-content: center;
- }
- .moreBtn {
- font-weight: 900;
- letter-spacing: -0.01em;
- }
- :deep(.pageHero) {
- background-image: url("../assets/images/bg5.jpg");
- background-size: cover, cover;
- background-position: center, center;
- background-repeat: no-repeat, no-repeat;
- background-blend-mode: overlay;
- color: #111;
- border-bottom: none;
- padding: 80px 0;
- }
- :deep(.pageHero__subtitle) {
- color: #4f5055;
- }
- @media (max-width: 960px) {
- .caseGrid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- .tabBar {
- grid-template-columns: 1fr;
- height: auto;
- }
- .tabBtn {
- justify-content: space-between;
- padding: 0 16px;
- height: 64px;
- }
- .tabText {
- padding-bottom: 6px;
- }
- }
- @media (max-width: 560px) {
- .caseGrid {
- grid-template-columns: 1fr;
- }
- }
- </style>
|