case3.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <template>
  2. <div>
  3. <PageHero
  4. kicker=""
  5. title="智能注气"
  6. subtitle="围绕设备全生命周期,实现台账、巡检维保、预测维护与实时监控预警的闭环管理。"
  7. >
  8. <template #actions>
  9. <RouterLink
  10. class="btn btn-primary"
  11. style="border-radius: 0"
  12. to="/contact"
  13. >获取产品资料</RouterLink
  14. >
  15. </template>
  16. </PageHero>
  17. <main class="case-content">
  18. <div class="caseLayout">
  19. <aside class="timelineCol" aria-label="项目实施时间线">
  20. <div class="timeline">
  21. <div class="timelineTrack" aria-hidden="true">
  22. <div class="timelineRail"></div>
  23. <div
  24. class="timelineFill"
  25. :style="{ transform: `scaleY(${progress})` }"
  26. ></div>
  27. </div>
  28. <ol class="timelineSteps" role="list">
  29. <li
  30. v-for="(s, idx) in steps"
  31. :key="s.key"
  32. class="timelineStep"
  33. :data-state="stepState(idx)"
  34. >
  35. <button
  36. type="button"
  37. class="timelineBtn"
  38. :aria-current="activeIndex === idx ? 'step' : 'false'"
  39. @click="scrollToStep(s.key)"
  40. >
  41. <span class="dot" aria-hidden="true"></span>
  42. <span class="stepText">
  43. <span class="stepTitle">{{ s.title }}</span>
  44. <span class="stepSub muted">{{ s.sub }}</span>
  45. </span>
  46. </button>
  47. </li>
  48. </ol>
  49. </div>
  50. </aside>
  51. <div class="contentCol">
  52. <!-- 项目背景 -->
  53. <section
  54. :ref="setStepEl('background')"
  55. id="step-background"
  56. class="case-section case-section--odd"
  57. >
  58. <div class="case-section__image">
  59. <img src="../../assets/images/case5.png" alt="设备管理场景" />
  60. </div>
  61. <div class="case-section__content">
  62. <h2>项目背景,突破传统注气瓶颈</h2>
  63. <p>
  64. XX
  65. 注气现场面临设备老旧、自动化程度低的双重困境,依赖大量人员现场值守,作业效率受限,参数调整滞后,且设备故障多为被动抢修,存在较大安全隐患与运维成本,亟需通过智能化改造实现降本增效。
  66. </p>
  67. </div>
  68. </section>
  69. <!-- 核心落地成果 -->
  70. <section
  71. :ref="setStepEl('delivery')"
  72. id="step-delivery"
  73. class="case-section case-section--even"
  74. >
  75. <div class="case-section__image">
  76. <img src="../../assets/images/case6.jpg" alt="数字化管控系统" />
  77. </div>
  78. <div class="case-section__content">
  79. <h2>核心打造,打造智能无人底座</h2>
  80. <p>
  81. 依托工业互联网平台,对现场老旧设备进行专项升级,加装远程控制器、智能阀门等核心组件,完成设备智能化改造。改造后设备全面接入平台,实现数据互通,为无人少人值守与智能调控奠定坚实硬件基础。
  82. </p>
  83. </div>
  84. </section>
  85. <!-- 预测性维护赋能 -->
  86. <section
  87. :ref="setStepEl('predictive')"
  88. id="step-predictive"
  89. class="case-section case-section--odd"
  90. >
  91. <div class="case-section__image">
  92. <img src="../../assets/images/case7.png" alt="预测性维护" />
  93. </div>
  94. <div class="case-section__content">
  95. <h2>智能调参与预测维护,双轮驱动提质增效</h2>
  96. <p>
  97. 升级后的设备联动平台
  98. AI算法,实时采集压力、温度、流量等核心数据,自动分析工况并动态调整注入参数,实现精准高效注气。同时,通过预测性维护技术精准预判设备潜在故障,变被动抢修为主动预防,提升设备管理水平。
  99. </p>
  100. </div>
  101. </section>
  102. <!-- 量化成效 -->
  103. <section
  104. :ref="setStepEl('kpi')"
  105. id="step-kpi"
  106. class="case-section case-section--even"
  107. >
  108. <div class="case-section__image">
  109. <img src="../../assets/images/case4.png" alt="量化成效展示" />
  110. </div>
  111. <div class="case-section__content">
  112. <h2>量化成效,实现安全高效转型</h2>
  113. <p>项目上线后,XX 注气现场管理效能全面跃升,核心成效显著:</p>
  114. </div>
  115. </section>
  116. </div>
  117. </div>
  118. </main>
  119. </div>
  120. </template>
  121. <script setup>
  122. import { computed, onBeforeUnmount, onMounted, ref } from "vue";
  123. import PageHero from "../../components/PageHero.vue";
  124. import { Icon } from "@iconify/vue";
  125. const steps = [
  126. { key: "background", title: "项目背景", sub: "问题识别与痛点分析" },
  127. { key: "delivery", title: "落地成果", sub: "系统上线与流程闭环" },
  128. { key: "predictive", title: "提质增效", sub: "提升设备管理水平" },
  129. { key: "kpi", title: "量化成效", sub: "指标提升与降本增效" },
  130. ];
  131. const activeIndex = ref(0);
  132. const progress = ref(0);
  133. const stepEls = ref({});
  134. const setStepEl = (key) => (el) => {
  135. if (!el) return;
  136. stepEls.value = { ...stepEls.value, [key]: el };
  137. };
  138. const prefersReducedMotion = () =>
  139. typeof window !== "undefined" &&
  140. window.matchMedia &&
  141. window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  142. const headerOffset = () => {
  143. if (typeof window === "undefined") return 72;
  144. const raw = getComputedStyle(document.documentElement)
  145. .getPropertyValue("--header-h")
  146. .trim();
  147. const n = Number.parseInt(raw, 10);
  148. return Number.isFinite(n) ? n : 72;
  149. };
  150. const clamp01 = (n) => Math.max(0, Math.min(1, n));
  151. const orderedEls = computed(() =>
  152. steps.map((s) => stepEls.value[s.key]).filter(Boolean),
  153. );
  154. const computeActiveIndex = (y) => {
  155. let idx = 0;
  156. for (let i = 0; i < steps.length; i++) {
  157. const el = stepEls.value[steps[i].key];
  158. if (!el) continue;
  159. const top = el.getBoundingClientRect().top + window.scrollY;
  160. if (y >= top) idx = i;
  161. }
  162. return idx;
  163. };
  164. const computeProgress = (y) => {
  165. const els = orderedEls.value;
  166. if (els.length < 2) return 0;
  167. const first = els[0];
  168. const last = els[els.length - 1];
  169. const start = first.getBoundingClientRect().top + window.scrollY;
  170. const end = last.getBoundingClientRect().bottom + window.scrollY;
  171. const span = Math.max(1, end - start);
  172. return clamp01((y - start) / span);
  173. };
  174. let raf = 0;
  175. const updateFromScroll = () => {
  176. raf = 0;
  177. if (typeof window === "undefined") return;
  178. const y = window.scrollY + headerOffset() + 130;
  179. activeIndex.value = computeActiveIndex(y);
  180. progress.value = computeProgress(y);
  181. };
  182. const onScroll = () => {
  183. if (raf) return;
  184. raf = window.requestAnimationFrame(updateFromScroll);
  185. };
  186. const scrollToStep = (key) => {
  187. const el = document.getElementById(`step-${key}`);
  188. if (!el) return;
  189. el.scrollIntoView({
  190. behavior: prefersReducedMotion() ? "auto" : "smooth",
  191. block: "start",
  192. });
  193. };
  194. const stepState = (idx) =>
  195. idx < activeIndex.value
  196. ? "done"
  197. : idx === activeIndex.value
  198. ? "active"
  199. : "todo";
  200. onMounted(() => {
  201. updateFromScroll();
  202. window.addEventListener("scroll", onScroll, { passive: true });
  203. window.addEventListener("resize", onScroll, { passive: true });
  204. });
  205. onBeforeUnmount(() => {
  206. window.removeEventListener("scroll", onScroll);
  207. window.removeEventListener("resize", onScroll);
  208. if (raf) window.cancelAnimationFrame(raf);
  209. });
  210. </script>
  211. <style scoped>
  212. .case-content {
  213. max-width: 1300px;
  214. margin: 0 auto;
  215. padding: 60px 20px;
  216. }
  217. .caseLayout {
  218. display: grid;
  219. grid-template-columns: 280px 1fr;
  220. gap: 26px;
  221. align-items: start;
  222. }
  223. .timelineCol {
  224. position: relative;
  225. height: 100%;
  226. min-height: 100vh;
  227. }
  228. .timeline {
  229. position: sticky;
  230. top: calc(var(--header-h) + 24px);
  231. padding: 10px 10px 12px 0;
  232. user-select: none;
  233. max-height: calc(100vh - var(--header-h) - 48px);
  234. overflow-y: auto;
  235. }
  236. .timelineTrack {
  237. position: absolute;
  238. left: 18px;
  239. top: 10px;
  240. bottom: 12px;
  241. border-radius: 1px;
  242. width: 2px;
  243. }
  244. .timelineRail {
  245. position: absolute;
  246. inset: 0;
  247. background: rgba(15, 23, 42, 0.12);
  248. border-radius: 999px;
  249. }
  250. .timelineFill {
  251. position: absolute;
  252. inset: 0;
  253. background: linear-gradient(180deg, #1d4ed8, #0ea5e9);
  254. border-radius: 999px;
  255. transform: scaleY(0);
  256. transform-origin: top;
  257. transition: transform 140ms linear;
  258. will-change: transform;
  259. }
  260. .timelineSteps {
  261. margin: 0;
  262. padding: 0 0 0 2px;
  263. list-style: none;
  264. display: grid;
  265. gap: 10px;
  266. }
  267. .timelineStep {
  268. position: relative;
  269. }
  270. .timelineBtn {
  271. width: 100%;
  272. display: grid;
  273. grid-template-columns: 42px 1fr;
  274. gap: 12px;
  275. align-items: start;
  276. text-align: left;
  277. padding: 10px 12px 10px 0;
  278. background: transparent;
  279. border: 0;
  280. cursor: pointer;
  281. color: inherit;
  282. }
  283. .timelineBtn:focus-visible {
  284. outline: none;
  285. box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.14);
  286. border-radius: 12px;
  287. }
  288. .dot {
  289. width: 15px;
  290. height: 15px;
  291. border-radius: 999px;
  292. border: 1px solid rgba(15, 23, 42, 0.16);
  293. background: rgba(255, 255, 255, 0.92);
  294. display: grid;
  295. place-items: center;
  296. margin-left: 10px;
  297. margin-top: 2px;
  298. transition:
  299. background-color 220ms ease,
  300. border-color 220ms ease,
  301. box-shadow 220ms ease;
  302. }
  303. .stepText {
  304. display: grid;
  305. gap: 3px;
  306. min-width: 0;
  307. }
  308. .stepTitle {
  309. /* font-weight: 900; */
  310. letter-spacing: -0.01em;
  311. line-height: 1.2;
  312. }
  313. .stepSub {
  314. font-size: 12px;
  315. line-height: 1.35;
  316. }
  317. .timelineStep[data-state="active"] .dot {
  318. transform: scale(1.12);
  319. border-color: rgba(29, 78, 216, 0.38);
  320. box-shadow:
  321. 0 0 0 6px rgba(37, 99, 235, 0.12),
  322. 0 10px 24px rgba(29, 78, 216, 0.18);
  323. background: #1d4ed8;
  324. }
  325. .timelineStep[data-state="done"] .dot {
  326. background: linear-gradient(180deg, #1d4ed8, #0ea5e9);
  327. border-color: transparent;
  328. box-shadow: 0 4px 12px rgba(29, 78, 216, 0.3);
  329. }
  330. .timelineStep[data-state="todo"] .stepTitle {
  331. color: rgba(2, 6, 23, 0.72);
  332. }
  333. .case-section {
  334. display: flex;
  335. margin-bottom: 80px;
  336. position: relative;
  337. scroll-margin-top: calc(var(--header-h) + 120px);
  338. }
  339. .case-section--odd {
  340. flex-direction: row;
  341. }
  342. .case-section--even {
  343. flex-direction: row-reverse;
  344. }
  345. .case-section__image {
  346. flex: 1;
  347. min-width: 300px;
  348. overflow: hidden;
  349. }
  350. .case-section__image img {
  351. width: 100%;
  352. height: auto;
  353. display: block;
  354. }
  355. .case-section__content {
  356. flex: 1;
  357. padding: 30px;
  358. /*
  359. box-shadow: 0 10px 40px rgba(15, 23, 42, 0.1); */
  360. margin-left: 20px;
  361. margin-right: 20px;
  362. }
  363. .case-section--even .case-section__content {
  364. margin-left: 20px;
  365. margin-right: 20px;
  366. }
  367. .case-section__content h2 {
  368. /* color: var(--brand-700); */
  369. font-size: 24px;
  370. margin-bottom: 16px;
  371. line-height: 1.3;
  372. }
  373. .case-section__content p {
  374. color: var(--slate-700);
  375. font-size: 16px;
  376. line-height: 1.8;
  377. }
  378. @media (max-width: 900px) {
  379. .caseLayout {
  380. grid-template-columns: 1fr;
  381. gap: 10px;
  382. }
  383. .timelineCol {
  384. display: none;
  385. }
  386. .case-section {
  387. flex-direction: column;
  388. }
  389. .case-section__image {
  390. order: 1;
  391. margin-bottom: 20px;
  392. }
  393. .case-section__content {
  394. order: 2;
  395. margin-left: 0;
  396. margin-right: 0;
  397. padding: 20px;
  398. }
  399. .case-section--even .case-section__content {
  400. order: 2;
  401. }
  402. }
  403. @media (max-width: 600px) {
  404. .case-content {
  405. padding: 40px 15px;
  406. }
  407. .case-section__content {
  408. padding: 15px;
  409. }
  410. }
  411. :deep(.pageHero) {
  412. background-image: url("../../assets/images/bg5.jpg");
  413. background-size: cover, cover;
  414. background-position: center, center;
  415. background-repeat: no-repeat, no-repeat;
  416. background-blend-mode: overlay;
  417. color: #111;
  418. border-bottom: none;
  419. padding: 80px 0;
  420. }
  421. :deep(.pageHero__subtitle) {
  422. color: #4f5055;
  423. }
  424. </style>