CasesView.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. <script setup>
  2. import { Icon } from "@iconify/vue";
  3. import { computed, onBeforeUnmount, onMounted, ref } from "vue";
  4. import PageHero from "../components/PageHero.vue";
  5. import Reveal from "../components/motion/Reveal.vue";
  6. import bannerUrl from "../assets/images/banner.jpg?url";
  7. import bgUrl from "../assets/images/bg.jpg?url";
  8. const activeTab = ref("equipment");
  9. const expanded = ref({
  10. equipment: false,
  11. qhse: false,
  12. injection: false,
  13. });
  14. const prefersReducedMotion = () =>
  15. typeof window !== "undefined" &&
  16. window.matchMedia &&
  17. window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  18. const categories = [
  19. {
  20. key: "equipment",
  21. title: "设备管理",
  22. lead:
  23. "覆盖台账、巡检、维保、预测性维护到告警处置的全生命周期闭环,让设备运行更稳定、维护更可控。",
  24. icon: "lucide:cpu",
  25. accent: "rgba(37, 99, 235, 0.9)",
  26. cover: bannerUrl,
  27. cases: [
  28. {
  29. title: "设备台账与参数标准化",
  30. desc: "统一资产主数据、参数口径与编码规则,沉淀可复用的设备数据底座。",
  31. tags: ["台账治理", "主数据", "标准化"],
  32. metrics: ["资产一致率 ≥99%", "新增设备上线 T+1", "编码规则统一"],
  33. },
  34. {
  35. title: "巡检计划与移动端执行闭环",
  36. desc: "计划自动排程、移动端签到拍照、异常一键上报,形成巡检闭环追踪。",
  37. tags: ["巡检", "移动端", "闭环"],
  38. metrics: ["漏检率≈0", "巡检效率 +30%", "异常处置可追溯"],
  39. },
  40. {
  41. title: "智能维保与工单联动",
  42. desc: "维保策略与备件、人员、工时联动,工单流转可视化,降低运维成本。",
  43. tags: ["维保", "工单", "备件"],
  44. metrics: ["工单闭环率 ≥98%", "平均修复时长 -20%", "备件周转 +15%"],
  45. },
  46. {
  47. title: "预测性维护与健康度评估",
  48. desc: "多源数据融合评估健康度,提前预警故障风险,避免非计划停机。",
  49. tags: ["预测性维护", "健康度", "预警"],
  50. metrics: ["停机 -20%", "维修成本 -12%", "告警准确率 +15%"],
  51. },
  52. {
  53. title: "设备接入与状态监测(SCADA/IoT)",
  54. desc: "统一采集与边缘接入,实时监测关键参数,支撑告警与趋势分析。",
  55. tags: ["接入", "监测", "边缘"],
  56. metrics: ["采集点位 6,000+", "秒级刷新", "告警规则可配置"],
  57. },
  58. {
  59. title: "异常告警与处置SOP",
  60. desc: "告警分级、自动派单与SOP引导,确保异常快速定位与闭环处置。",
  61. tags: ["告警", "SOP", "处置"],
  62. metrics: ["响应时长 -35%", "误报率下降", "处置闭环可审计"],
  63. },
  64. {
  65. title: "润滑与点检管理",
  66. desc: "点检路线、标准作业与耗材管理协同,减少人为遗漏与重复劳动。",
  67. tags: ["点检", "润滑", "耗材"],
  68. metrics: ["点检准时率 ≥99%", "重复工时 -18%", "耗材可追溯"],
  69. },
  70. {
  71. title: "关键设备KPI看板与对标",
  72. desc: "以停机、故障率、MTBF/MTTR为核心指标,对标分析并推动持续改进。",
  73. tags: ["KPI", "看板", "对标"],
  74. metrics: ["指标口径统一", "月度复盘固化", "改进项可跟踪"],
  75. },
  76. ],
  77. },
  78. {
  79. key: "qhse",
  80. title: "QHSE",
  81. lead:
  82. "将质量、健康、安全、环境融入业务流程:从风险识别到整改闭环,让合规与效率同时提升。",
  83. icon: "lucide:shield-check",
  84. accent: "rgba(14, 116, 144, 0.92)",
  85. cover: bgUrl,
  86. cases: [
  87. {
  88. title: "隐患排查治理与整改闭环",
  89. desc: "隐患登记、分级分责、整改验收一体化,形成闭环追踪与审计留痕。",
  90. tags: ["隐患", "整改", "闭环"],
  91. metrics: ["闭环率 ≥98%", "复发率下降", "审计留痕完整"],
  92. },
  93. {
  94. title: "作业票与高风险作业管控",
  95. desc: "动火、受限空间等高风险作业票线上化,联动审批、交底与现场验证。",
  96. tags: ["作业票", "审批", "现场验证"],
  97. metrics: ["审批时长 -40%", "票证合规率提升", "现场校验可追溯"],
  98. },
  99. {
  100. title: "风险分级管控与双重预防",
  101. desc: "风险辨识、分级与管控措施落地,叠加隐患治理形成双重预防机制。",
  102. tags: ["风险分级", "双重预防", "措施"],
  103. metrics: ["风险台账统一", "措施执行可核查", "复核机制固化"],
  104. },
  105. {
  106. title: "应急预案与演练管理",
  107. desc: "预案库、演练计划、过程记录与复盘改进,提升应急协同与响应效率。",
  108. tags: ["应急", "演练", "复盘"],
  109. metrics: ["演练覆盖率提升", "响应流程标准化", "复盘问题闭环"],
  110. },
  111. {
  112. title: "质量追溯与异常闭环",
  113. desc: "打通过程数据与质量规则,异常自动触发处置流程,提升产品/工程质量。",
  114. tags: ["质量", "追溯", "异常"],
  115. metrics: ["不良率下降", "追溯时间 -80%", "异常闭环 T+1"],
  116. },
  117. {
  118. title: "环保监测与排放合规",
  119. desc: "在线监测、阈值预警与报表归档,支撑排放合规与监管报送。",
  120. tags: ["环保", "监测", "合规"],
  121. metrics: ["预警及时", "报表自动生成", "监管口径一致"],
  122. },
  123. {
  124. title: "承包商HSE准入与评价",
  125. desc: "资质准入、培训考试、现场评价与黑白名单管理,降低外协风险。",
  126. tags: ["承包商", "准入", "评价"],
  127. metrics: ["准入效率提升", "违规率下降", "评价结果可追溯"],
  128. },
  129. {
  130. title: "培训考试与证书到期提醒",
  131. desc: "培训计划、在线考试、证书管理与到期提醒,保障人员资质合规。",
  132. tags: ["培训", "考试", "证书"],
  133. metrics: ["到期漏管≈0", "培训覆盖率提升", "记录可审计"],
  134. },
  135. ],
  136. },
  137. {
  138. key: "injection",
  139. title: "智能注气",
  140. lead:
  141. "围绕注气作业计划、过程监控与效果评估,联动工艺参数与产量数据,实现可控、可追溯、可优化。",
  142. icon: "lucide:wind",
  143. accent: "rgba(2, 132, 199, 0.92)",
  144. cover: bannerUrl,
  145. cases: [
  146. {
  147. title: "注气计划编排与批量下发",
  148. desc: "按井/站点编排注气计划,批量下发到现场执行端,确保节奏一致。",
  149. tags: ["计划", "下发", "协同"],
  150. metrics: ["计划变更可追溯", "下发效率提升", "执行偏差可视化"],
  151. },
  152. {
  153. title: "注气过程监控与异常预警",
  154. desc: "实时监控压力、流量、温度等关键参数,异常自动预警并联动处置。",
  155. tags: ["监控", "预警", "处置"],
  156. metrics: ["秒级监控", "异常定位更快", "告警规则可配置"],
  157. },
  158. {
  159. title: "注气工艺参数优化建议",
  160. desc: "基于历史数据与工况特征,给出参数窗口建议,提升作业稳定性。",
  161. tags: ["优化", "参数窗口", "建议"],
  162. metrics: ["波动降低", "能耗可控", "经验可沉淀"],
  163. },
  164. {
  165. title: "注气效果评估与对比分析",
  166. desc: "联动产量、压力与含水等指标,对比评估不同策略效果,支撑决策。",
  167. tags: ["评估", "对比", "决策"],
  168. metrics: ["评估口径统一", "策略对比直观", "月度复盘固化"],
  169. },
  170. {
  171. title: "井站联动与作业记录归档",
  172. desc: "井-站-管网联动视图,作业记录自动归档,满足追溯与审计需求。",
  173. tags: ["联动", "归档", "追溯"],
  174. metrics: ["记录完整", "审计便捷", "事件链条清晰"],
  175. },
  176. {
  177. title: "设备运行状态与注气策略联动",
  178. desc: "联动压缩机/阀组状态,自动约束策略边界,降低设备风险与误操作。",
  179. tags: ["联动", "约束", "安全"],
  180. metrics: ["误操作降低", "风险边界清晰", "联动规则可配置"],
  181. },
  182. {
  183. title: "能耗与成本核算(注气侧)",
  184. desc: "按作业、井、班组维度核算能耗与成本,为优化与考核提供依据。",
  185. tags: ["能耗", "成本", "核算"],
  186. metrics: ["核算自动化", "成本结构清晰", "对标可用"],
  187. },
  188. {
  189. title: "注气数据治理与口径统一",
  190. desc: "统一注气数据口径、指标体系与采集规范,支撑长期运营与模型迭代。",
  191. tags: ["数据治理", "指标体系", "规范"],
  192. metrics: ["口径统一", "数据可用性提升", "资产沉淀"],
  193. },
  194. ],
  195. },
  196. ];
  197. const tabItems = computed(() =>
  198. categories.map((c) => ({
  199. ...c,
  200. count: c.cases.length,
  201. })),
  202. );
  203. const maxPreview = 6;
  204. const visibleCases = (cat) =>
  205. expanded.value[cat.key] ? cat.cases : cat.cases.slice(0, maxPreview);
  206. const toggleExpand = (key) => {
  207. expanded.value = { ...expanded.value, [key]: !expanded.value[key] };
  208. };
  209. const scrollToCategory = (key) => {
  210. const el = document.getElementById(`case-${key}`);
  211. if (!el) return;
  212. activeTab.value = key;
  213. el.scrollIntoView({
  214. behavior: prefersReducedMotion() ? "auto" : "smooth",
  215. block: "start",
  216. });
  217. };
  218. let io = null;
  219. onMounted(() => {
  220. if (typeof window === "undefined") return;
  221. if (!("IntersectionObserver" in window)) return;
  222. const headerHRaw = getComputedStyle(document.documentElement)
  223. .getPropertyValue("--header-h")
  224. .trim();
  225. const headerH = Number.parseInt(headerHRaw, 10);
  226. const topOffset = Number.isFinite(headerH) ? headerH : 72;
  227. const sections = categories
  228. .map((c) => document.getElementById(`case-${c.key}`))
  229. .filter(Boolean);
  230. io = new IntersectionObserver(
  231. (entries) => {
  232. const visible = entries
  233. .filter((e) => e.isIntersecting)
  234. .sort(
  235. (a, b) =>
  236. (a.boundingClientRect.top ?? 0) - (b.boundingClientRect.top ?? 0),
  237. )[0];
  238. if (!visible) return;
  239. const id = visible.target.getAttribute("id") || "";
  240. const key = id.replace("case-", "");
  241. if (key) activeTab.value = key;
  242. },
  243. {
  244. threshold: 0.22,
  245. rootMargin: `-${topOffset + 10}px 0px -55% 0px`,
  246. },
  247. );
  248. for (const s of sections) io.observe(s);
  249. });
  250. onBeforeUnmount(() => {
  251. if (io) io.disconnect();
  252. });
  253. </script>
  254. <template>
  255. <div class="casesView">
  256. <PageHero
  257. kicker="客户案例"
  258. title="案例分为三大类:设备管理、QHSE、智能注气"
  259. subtitle="每个分类沉淀了多项可复用的落地案例与最佳实践,覆盖从方案设计到运营闭环的关键环节。"
  260. >
  261. <template #actions>
  262. <RouterLink class="btn btn-primary" style="border-radius: 0" to="/contact"
  263. >留言咨询</RouterLink
  264. >
  265. </template>
  266. </PageHero>
  267. <nav class="tabBar" aria-label="案例分类导航">
  268. <button
  269. v-for="t in tabItems"
  270. :key="t.key"
  271. type="button"
  272. class="tabBtn"
  273. :class="{ 'is-active': activeTab === t.key }"
  274. @click="scrollToCategory(t.key)"
  275. >
  276. <Icon :icon="t.icon" class="tabIcon" />
  277. <span class="tabText">{{ t.title }}</span>
  278. <span class="tabCount">{{ t.count }}</span>
  279. </button>
  280. </nav>
  281. <section class="section">
  282. <div class="container">
  283. <div class="stack">
  284. <section
  285. v-for="(cat, catIdx) in categories"
  286. :key="cat.key"
  287. class="caseSection"
  288. :id="`case-${cat.key}`"
  289. :style="{ '--accent': cat.accent }"
  290. >
  291. <Reveal as="header" class="caseHead" :delay="40 + catIdx * 80">
  292. <div class="caseHeadTop">
  293. <span class="caseBadge">分类</span>
  294. <div class="caseHeadTitle">
  295. <Icon :icon="cat.icon" class="caseHeadIcon" />
  296. <h2 class="h2">{{ cat.title }}</h2>
  297. </div>
  298. </div>
  299. <p class="muted caseLead">{{ cat.lead }}</p>
  300. <div class="caseHeadActions">
  301. <RouterLink class="btn btn-ghost" style="border-radius: 0" to="/contact"
  302. >对接同类项目</RouterLink
  303. >
  304. </div>
  305. </Reveal>
  306. <div class="caseGrid">
  307. <Reveal
  308. v-for="(c, idx) in visibleCases(cat)"
  309. :key="c.title"
  310. as="article"
  311. class="card caseCard hover-lift"
  312. :delay="60 + idx * 60"
  313. >
  314. <div class="caseCardTop">
  315. <div class="caseCardLine" aria-hidden="true"></div>
  316. <div class="caseCardTitle">{{ c.title }}</div>
  317. <div class="muted caseCardDesc">{{ c.desc }}</div>
  318. </div>
  319. <div class="caseMeta">
  320. <div class="caseTags" aria-label="标签">
  321. <span v-for="t in c.tags" :key="t" class="tag">{{ t }}</span>
  322. </div>
  323. <div class="caseMetrics" aria-label="指标">
  324. <span v-for="m in c.metrics" :key="m" class="metric">{{ m }}</span>
  325. </div>
  326. </div>
  327. </Reveal>
  328. </div>
  329. <div class="caseMore">
  330. <button
  331. v-if="cat.cases.length > maxPreview"
  332. type="button"
  333. class="btn btn-link moreBtn"
  334. @click="toggleExpand(cat.key)"
  335. >
  336. {{ expanded[cat.key] ? "收起" : `展开更多(${cat.cases.length - maxPreview})` }}
  337. </button>
  338. </div>
  339. </section>
  340. </div>
  341. </div>
  342. </section>
  343. </div>
  344. </template>
  345. <style scoped>
  346. .casesView {
  347. --tab-bg: rgba(255, 255, 255, 0.92);
  348. }
  349. .tabBar {
  350. position: sticky;
  351. top: var(--header-h);
  352. z-index: 20;
  353. display: grid;
  354. grid-template-columns: repeat(3, minmax(0, 1fr));
  355. background: #eef5f8;
  356. height: 76px;
  357. margin: 10px 0 18px;
  358. border: 0;
  359. border-radius: 0;
  360. }
  361. .tabBtn {
  362. height: 76px;
  363. border: 0;
  364. background: #eef5f8;
  365. padding: 0 18px;
  366. display: inline-flex;
  367. align-items: center;
  368. justify-content: center;
  369. gap: 10px;
  370. color: var(--slate-700);
  371. cursor: pointer;
  372. transition:
  373. transform 160ms ease,
  374. box-shadow 160ms ease,
  375. color 160ms ease,
  376. background-color 160ms ease;
  377. }
  378. .tabBtn.is-active {
  379. color: var(--brand-700);
  380. background-color: #fff;
  381. }
  382. .tabBtn:focus-visible {
  383. outline: none;
  384. box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.22);
  385. }
  386. .tabIcon {
  387. width: 18px;
  388. height: 18px;
  389. }
  390. .tabText {
  391. display: inline-block;
  392. white-space: nowrap;
  393. font-weight: 900;
  394. letter-spacing: -0.01em;
  395. padding-bottom: 10px;
  396. background-image: linear-gradient(currentColor, currentColor);
  397. background-repeat: no-repeat;
  398. background-position: 0 100%;
  399. background-size: 0 2px;
  400. transition: background-size 160ms ease;
  401. }
  402. .tabBtn.is-active .tabText {
  403. background-size: 100% 2px;
  404. }
  405. .tabCount {
  406. font-size: 12px;
  407. font-weight: 900;
  408. padding: 4px 8px;
  409. border-radius: 999px;
  410. border: 1px solid rgba(15, 23, 42, 0.12);
  411. background: rgba(255, 255, 255, 0.72);
  412. color: rgba(2, 6, 23, 0.72);
  413. }
  414. .stack {
  415. display: grid;
  416. gap: 26px;
  417. }
  418. .caseSection {
  419. scroll-margin-top: calc(var(--header-h) + 88px);
  420. padding: 18px 0 10px;
  421. }
  422. .caseHead {
  423. padding: 18px 4px 14px;
  424. display: grid;
  425. gap: 10px;
  426. }
  427. .caseHeadTop {
  428. display: flex;
  429. align-items: center;
  430. justify-content: space-between;
  431. gap: 14px;
  432. flex-wrap: wrap;
  433. }
  434. .caseBadge {
  435. font-size: 12px;
  436. font-weight: 900;
  437. letter-spacing: 0.12em;
  438. color: rgba(2, 6, 23, 0.62);
  439. background: rgba(15, 23, 42, 0.04);
  440. border: 1px solid rgba(15, 23, 42, 0.08);
  441. padding: 6px 10px;
  442. border-radius: 999px;
  443. flex: 0 0 auto;
  444. }
  445. .caseHeadTitle {
  446. display: inline-flex;
  447. align-items: center;
  448. gap: 10px;
  449. }
  450. .caseHeadIcon {
  451. width: 20px;
  452. height: 20px;
  453. color: var(--accent);
  454. }
  455. .caseLead {
  456. max-width: 92ch;
  457. }
  458. .caseHeadActions {
  459. margin-top: 2px;
  460. }
  461. .caseGrid {
  462. display: grid;
  463. grid-template-columns: repeat(3, minmax(0, 1fr));
  464. gap: 18px;
  465. margin-top: 10px;
  466. }
  467. .caseCard {
  468. border: 1px solid var(--border);
  469. border-top: 0;
  470. box-shadow: 0 16px 42px rgba(2, 6, 23, 0.08);
  471. background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), #fff);
  472. }
  473. .caseCardTop {
  474. padding: 18px 18px 10px;
  475. }
  476. .caseCardLine {
  477. height: 2px;
  478. width: 100%;
  479. background: linear-gradient(90deg, var(--accent), rgba(2, 6, 23, 0));
  480. margin-bottom: 12px;
  481. }
  482. .caseCardTitle {
  483. font-weight: 900;
  484. letter-spacing: -0.02em;
  485. line-height: 1.25;
  486. }
  487. .caseCardDesc {
  488. margin-top: 8px;
  489. line-height: 1.75;
  490. }
  491. .caseMeta {
  492. padding: 8px 18px 18px;
  493. display: grid;
  494. gap: 12px;
  495. }
  496. .caseTags {
  497. display: flex;
  498. flex-wrap: wrap;
  499. gap: 8px;
  500. }
  501. .tag {
  502. display: inline-flex;
  503. align-items: center;
  504. padding: 6px 10px;
  505. border-radius: 999px;
  506. background: rgba(15, 23, 42, 0.03);
  507. border: 1px solid var(--border);
  508. font-weight: 700;
  509. color: var(--slate-700);
  510. font-size: 12px;
  511. }
  512. .caseMetrics {
  513. display: flex;
  514. flex-wrap: wrap;
  515. gap: 10px;
  516. }
  517. .metric {
  518. display: inline-flex;
  519. align-items: center;
  520. padding: 6px 10px;
  521. background: rgba(15, 23, 42, 0.03);
  522. border: 1px solid var(--border);
  523. border-left: 2px solid var(--accent);
  524. font-weight: 800;
  525. color: rgba(2, 6, 23, 0.78);
  526. font-size: 12px;
  527. }
  528. .caseMore {
  529. margin-top: 10px;
  530. display: flex;
  531. justify-content: center;
  532. }
  533. .moreBtn {
  534. font-weight: 900;
  535. letter-spacing: -0.01em;
  536. }
  537. :deep(.pageHero) {
  538. background-image: url("../assets/images/bg5.jpg");
  539. background-size: cover, cover;
  540. background-position: center, center;
  541. background-repeat: no-repeat, no-repeat;
  542. background-blend-mode: overlay;
  543. color: #111;
  544. border-bottom: none;
  545. padding: 80px 0;
  546. }
  547. :deep(.pageHero__subtitle) {
  548. color: #4f5055;
  549. }
  550. @media (max-width: 960px) {
  551. .caseGrid {
  552. grid-template-columns: repeat(2, minmax(0, 1fr));
  553. }
  554. .tabBar {
  555. grid-template-columns: 1fr;
  556. height: auto;
  557. }
  558. .tabBtn {
  559. justify-content: space-between;
  560. padding: 0 16px;
  561. height: 64px;
  562. }
  563. .tabText {
  564. padding-bottom: 6px;
  565. }
  566. }
  567. @media (max-width: 560px) {
  568. .caseGrid {
  569. grid-template-columns: 1fr;
  570. }
  571. }
  572. </style>