main.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. /**
  2. * 亿图腾 · 浠艾福 — 企业官网交互脚本
  3. * Mobile nav, scroll animations, header effects
  4. */
  5. document.addEventListener('DOMContentLoaded', () => {
  6. // --------------------------------------------------------
  7. // 1. Mobile Nav Toggle
  8. // --------------------------------------------------------
  9. const navToggle = document.getElementById('navToggle');
  10. const nav = document.getElementById('nav');
  11. if (navToggle && nav) {
  12. navToggle.addEventListener('click', () => {
  13. navToggle.classList.toggle('active');
  14. nav.classList.toggle('open');
  15. const isOpen = nav.classList.contains('open');
  16. navToggle.setAttribute('aria-label', isOpen ? '关闭导航' : '切换导航');
  17. });
  18. // Close nav on link click
  19. nav.querySelectorAll('.nav-link').forEach(link => {
  20. link.addEventListener('click', () => {
  21. navToggle.classList.remove('active');
  22. nav.classList.remove('open');
  23. navToggle.setAttribute('aria-label', '切换导航');
  24. });
  25. });
  26. }
  27. // --------------------------------------------------------
  28. // 2. Header shadow on scroll
  29. // --------------------------------------------------------
  30. const header = document.getElementById('header');
  31. if (header) {
  32. let ticking = false;
  33. window.addEventListener('scroll', () => {
  34. if (!ticking) {
  35. requestAnimationFrame(() => {
  36. header.classList.toggle('scrolled', window.scrollY > 40);
  37. ticking = false;
  38. });
  39. ticking = true;
  40. }
  41. }, { passive: true });
  42. }
  43. // --------------------------------------------------------
  44. // 3. Active nav link tracking (Intersection Observer)
  45. // --------------------------------------------------------
  46. const sections = document.querySelectorAll('section[id]');
  47. const navLinks = document.querySelectorAll('.nav-link');
  48. if (sections.length && navLinks.length) {
  49. const observerOptions = {
  50. rootMargin: '-50% 0px -50% 0px',
  51. threshold: 0
  52. };
  53. const sectionObserver = new IntersectionObserver((entries) => {
  54. entries.forEach(entry => {
  55. if (entry.isIntersecting) {
  56. const id = entry.target.getAttribute('id');
  57. navLinks.forEach(link => {
  58. link.classList.remove('active');
  59. if (link.getAttribute('href') === `#${id}`) {
  60. link.classList.add('active');
  61. }
  62. });
  63. }
  64. });
  65. }, observerOptions);
  66. sections.forEach(section => sectionObserver.observe(section));
  67. }
  68. // --------------------------------------------------------
  69. // 4. Scroll-triggered reveal animations
  70. // --------------------------------------------------------
  71. const revealElements = document.querySelectorAll(
  72. '.about-card, .dimension-card, .feature-card, .role-card, ' +
  73. '.stat, .contact-item, .cta-card, .product-quote, .pillar-card'
  74. );
  75. if (revealElements.length) {
  76. // Add reveal class
  77. revealElements.forEach(el => el.classList.add('reveal'));
  78. const revealObserver = new IntersectionObserver((entries) => {
  79. entries.forEach(entry => {
  80. if (entry.isIntersecting) {
  81. // Stagger animation based on index
  82. const siblings = Array.from(entry.target.parentElement.children)
  83. .filter(child => child.classList.contains('reveal'));
  84. const index = siblings.indexOf(entry.target);
  85. entry.target.style.transitionDelay = `${index * 80}ms`;
  86. entry.target.classList.add('visible');
  87. revealObserver.unobserve(entry.target);
  88. }
  89. });
  90. }, {
  91. threshold: 0.15,
  92. rootMargin: '0px 0px -40px 0px'
  93. });
  94. revealElements.forEach(el => revealObserver.observe(el));
  95. }
  96. // --------------------------------------------------------
  97. // 5. Smooth scroll for anchor links (progressive enhancement)
  98. // --------------------------------------------------------
  99. document.querySelectorAll('a[href^="#"]').forEach(anchor => {
  100. anchor.addEventListener('click', (e) => {
  101. const targetId = anchor.getAttribute('href');
  102. if (targetId === '#') return;
  103. const target = document.querySelector(targetId);
  104. if (target) {
  105. e.preventDefault();
  106. const headerOffset = 64; // --header-height
  107. const targetPosition = target.getBoundingClientRect().top + window.scrollY - headerOffset;
  108. window.scrollTo({
  109. top: targetPosition,
  110. behavior: 'smooth'
  111. });
  112. }
  113. });
  114. });
  115. // --------------------------------------------------------
  116. // 6. Orbit animation pause on hover (accessibility)
  117. // --------------------------------------------------------
  118. const orbitItems = document.querySelectorAll('.orbit-item');
  119. orbitItems.forEach(item => {
  120. item.addEventListener('mouseenter', () => {
  121. item.style.animationPlayState = 'paused';
  122. });
  123. item.addEventListener('mouseleave', () => {
  124. item.style.animationPlayState = 'running';
  125. });
  126. });
  127. // --------------------------------------------------------
  128. // 7. Performance: debounced resize handler
  129. // --------------------------------------------------------
  130. let resizeTimer;
  131. window.addEventListener('resize', () => {
  132. clearTimeout(resizeTimer);
  133. resizeTimer = setTimeout(() => {
  134. // Close mobile nav on resize to desktop
  135. if (window.innerWidth > 768 && nav && nav.classList.contains('open')) {
  136. navToggle.classList.remove('active');
  137. nav.classList.remove('open');
  138. navToggle.setAttribute('aria-label', '切换导航');
  139. }
  140. // Redraw wuxing canvas on resize
  141. drawWuxingDiagram();
  142. }, 200);
  143. }, { passive: true });
  144. // --------------------------------------------------------
  145. // 8. Wuxing Diagram — Canvas 2D (redesigned elegant version)
  146. // --------------------------------------------------------
  147. function drawWuxingDiagram() {
  148. const canvas = document.getElementById('wuxingCanvas');
  149. if (!canvas) return;
  150. const container = canvas.parentElement;
  151. const size = container.clientWidth;
  152. if (size < 10) return;
  153. const dpr = window.devicePixelRatio || 1;
  154. canvas.width = size * dpr;
  155. canvas.height = size * dpr;
  156. canvas.style.width = size + 'px';
  157. canvas.style.height = size + 'px';
  158. const ctx = canvas.getContext('2d');
  159. ctx.scale(dpr, dpr);
  160. const cx = size / 2;
  161. const cy = size / 2;
  162. const outerR = size * 0.38;
  163. // ---- calculate 5 outer vertices ----
  164. function calcPentagonVertices(cx, cy, radius) {
  165. const pts = [];
  166. for (let i = 0; i < 5; i++) {
  167. const angle = -Math.PI / 2 + i * 2 * Math.PI / 5;
  168. pts.push({
  169. x: cx + radius * Math.cos(angle),
  170. y: cy + radius * Math.sin(angle)
  171. });
  172. }
  173. return pts;
  174. }
  175. const outer = calcPentagonVertices(cx, cy, outerR);
  176. // dimension data [身, 智, 富, 行, 心] (top clockwise)
  177. const dims = [
  178. { name: '身', element: '土', color: '#5D4037', bg: '#F5EFEB', desc: '主动健康' },
  179. { name: '智', element: '金', color: '#C0A060', bg: '#F9F4EC', desc: '赋能未来' },
  180. { name: '富', element: '水', color: '#0EA5E9', bg: '#ECFEFF', desc: '利他创富' },
  181. { name: '行', element: '木', color: '#10B981', bg: '#ECFDF5', desc: '人际和谐' },
  182. { name: '心', element: '火', color: '#EF4444', bg: '#FEF2F2', desc: '子女传承' }
  183. ];
  184. // ---- helpers ----
  185. function midPoint(p1, p2) {
  186. return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
  187. }
  188. // ================================================================
  189. // 1. Background — soft radial glow
  190. // ================================================================
  191. var bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, outerR * 1.4);
  192. bgGrad.addColorStop(0, 'rgba(74, 155, 215, 0.04)');
  193. bgGrad.addColorStop(0.6, 'rgba(74, 155, 215, 0.02)');
  194. bgGrad.addColorStop(1, 'rgba(74, 155, 215, 0)');
  195. ctx.fillStyle = bgGrad;
  196. ctx.beginPath();
  197. ctx.arc(cx, cy, outerR * 1.4, 0, Math.PI * 2);
  198. ctx.fill();
  199. // ================================================================
  200. // 2. Outer orbit ring
  201. // ================================================================
  202. ctx.beginPath();
  203. ctx.arc(cx, cy, outerR + outerR * 0.08, 0, Math.PI * 2);
  204. ctx.strokeStyle = 'rgba(180, 170, 160, 0.25)';
  205. ctx.lineWidth = 1;
  206. ctx.setLineDash([3, 5]);
  207. ctx.stroke();
  208. ctx.setLineDash([]);
  209. // Inner orbit ring
  210. ctx.beginPath();
  211. ctx.arc(cx, cy, outerR * 0.55, 0, Math.PI * 2);
  212. ctx.strokeStyle = 'rgba(180, 170, 160, 0.12)';
  213. ctx.lineWidth = 1;
  214. ctx.setLineDash([2, 4]);
  215. ctx.stroke();
  216. ctx.setLineDash([]);
  217. // ================================================================
  218. // 3. 相克 (pentagram star) — drawn first so it sits behind
  219. // ================================================================
  220. var keOrder = [0, 2, 4, 1, 3]; // 身→富→心→智→行→身
  221. var keColor = 'rgba(74, 155, 215, 0.45)';
  222. var nodeR = Math.max(24, size * 0.068);
  223. for (var i = 0; i < 5; i++) {
  224. var from = outer[keOrder[i % 5]];
  225. var to = outer[keOrder[(i + 1) % 5]];
  226. // dashed line
  227. ctx.beginPath();
  228. ctx.moveTo(from.x, from.y);
  229. ctx.lineTo(to.x, to.y);
  230. ctx.strokeStyle = keColor;
  231. ctx.lineWidth = 1.5;
  232. ctx.setLineDash([4, 5]);
  233. ctx.stroke();
  234. ctx.setLineDash([]);
  235. // Arrow near the end (before the target node)
  236. var dx = to.x - from.x;
  237. var dy = to.y - from.y;
  238. var len = Math.sqrt(dx * dx + dy * dy) || 1;
  239. var arrowDist = nodeR + 8;
  240. var ax = to.x - (dx / len) * arrowDist;
  241. var ay = to.y - (dy / len) * arrowDist;
  242. var angle = Math.atan2(dy, dx);
  243. var s = 8;
  244. ctx.save();
  245. ctx.translate(ax, ay);
  246. ctx.rotate(angle);
  247. ctx.beginPath();
  248. ctx.moveTo(s, 0);
  249. ctx.lineTo(-s * 0.5, -s * 0.6);
  250. ctx.lineTo(-s * 0.5, s * 0.6);
  251. ctx.closePath();
  252. ctx.fillStyle = 'rgba(74, 155, 215, 0.6)';
  253. ctx.fill();
  254. ctx.restore();
  255. }
  256. // ================================================================
  257. // 4. 相生 (outer pentagon) — elegant gradient lines
  258. // ================================================================
  259. var shengColors = ['#4ECDC4', '#45B7AA', '#4ECDC4', '#45B7AA', '#4ECDC4'];
  260. for (var i = 0; i < 5; i++) {
  261. var from = outer[i];
  262. var to = outer[(i + 1) % 5];
  263. // Glow shadow under the line
  264. ctx.beginPath();
  265. ctx.moveTo(from.x, from.y);
  266. ctx.lineTo(to.x, to.y);
  267. ctx.strokeStyle = 'rgba(78, 205, 196, 0.15)';
  268. ctx.lineWidth = 6;
  269. ctx.shadowColor = 'rgba(78, 205, 196, 0.3)';
  270. ctx.shadowBlur = 8;
  271. ctx.stroke();
  272. ctx.shadowColor = 'transparent';
  273. ctx.shadowBlur = 0;
  274. // Main line with gradient
  275. var grad = ctx.createLinearGradient(from.x, from.y, to.x, to.y);
  276. grad.addColorStop(0, shengColors[i]);
  277. grad.addColorStop(1, shengColors[(i + 1) % 5]);
  278. ctx.beginPath();
  279. ctx.moveTo(from.x, from.y);
  280. ctx.lineTo(to.x, to.y);
  281. ctx.strokeStyle = grad;
  282. ctx.lineWidth = 2.5;
  283. ctx.lineCap = 'round';
  284. ctx.stroke();
  285. // Elegant arrow at midpoint
  286. var mp = midPoint(from, to);
  287. var angle = Math.atan2(to.y - from.y, to.x - from.x);
  288. var s = 11;
  289. ctx.save();
  290. ctx.translate(mp.x, mp.y);
  291. ctx.rotate(angle);
  292. ctx.beginPath();
  293. ctx.moveTo(s, 0);
  294. ctx.lineTo(-s * 0.4, -s * 0.5);
  295. ctx.lineTo(-s * 0.2, 0);
  296. ctx.lineTo(-s * 0.4, s * 0.5);
  297. ctx.closePath();
  298. ctx.fillStyle = 'rgba(78, 205, 196, 0.8)';
  299. ctx.fill();
  300. ctx.restore();
  301. }
  302. // ================================================================
  303. // 5. Dimension nodes — multi-layered polished circles
  304. // ================================================================
  305. for (var i = 0; i < 5; i++) {
  306. var v = outer[i];
  307. var d = dims[i];
  308. // Outer soft glow
  309. var glowGrad = ctx.createRadialGradient(v.x, v.y, nodeR * 0.3, v.x, v.y, nodeR * 2.2);
  310. glowGrad.addColorStop(0, d.color + '22');
  311. glowGrad.addColorStop(1, d.color + '00');
  312. ctx.fillStyle = glowGrad;
  313. ctx.beginPath();
  314. ctx.arc(v.x, v.y, nodeR * 2.2, 0, Math.PI * 2);
  315. ctx.fill();
  316. // Outer ring (stroke only)
  317. ctx.beginPath();
  318. ctx.arc(v.x, v.y, nodeR + 4, 0, Math.PI * 2);
  319. ctx.strokeStyle = d.color + '55';
  320. ctx.lineWidth = 1.5;
  321. ctx.stroke();
  322. // Inner white fill
  323. ctx.beginPath();
  324. ctx.arc(v.x, v.y, nodeR, 0, Math.PI * 2);
  325. ctx.fillStyle = d.bg;
  326. ctx.fill();
  327. // Colored border
  328. ctx.beginPath();
  329. ctx.arc(v.x, v.y, nodeR, 0, Math.PI * 2);
  330. ctx.strokeStyle = d.color;
  331. ctx.lineWidth = 2.5;
  332. ctx.stroke();
  333. // Soft inner highlight (top-left)
  334. var hlGrad = ctx.createRadialGradient(
  335. v.x - nodeR * 0.3, v.y - nodeR * 0.3, 0,
  336. v.x, v.y, nodeR
  337. );
  338. hlGrad.addColorStop(0, 'rgba(255,255,255,0.6)');
  339. hlGrad.addColorStop(0.5, 'rgba(255,255,255,0)');
  340. hlGrad.addColorStop(1, 'rgba(0,0,0,0.03)');
  341. ctx.beginPath();
  342. ctx.arc(v.x, v.y, nodeR, 0, Math.PI * 2);
  343. ctx.fillStyle = hlGrad;
  344. ctx.fill();
  345. // Dimension name
  346. var fs = Math.max(15, nodeR * 0.58);
  347. ctx.font = 'bold ' + fs + 'px "PingFang SC","Microsoft YaHei",sans-serif';
  348. ctx.textAlign = 'center';
  349. ctx.textBaseline = 'middle';
  350. ctx.fillStyle = d.color;
  351. ctx.fillText(d.name, v.x, v.y - fs * 0.25);
  352. // Element badge (small pill below name)
  353. var elemSize = Math.max(10, fs * 0.5);
  354. ctx.font = elemSize + 'px "PingFang SC","Microsoft YaHei",sans-serif';
  355. ctx.fillStyle = d.color;
  356. ctx.globalAlpha = 0.55;
  357. ctx.fillText(d.element, v.x, v.y + fs * 0.55);
  358. ctx.globalAlpha = 1;
  359. // Small decorative dot on outer ring (node edge highlight)
  360. for (var dAngle = 0; dAngle < 5; dAngle++) {
  361. var da = -Math.PI / 2 + dAngle * 2 * Math.PI / 5;
  362. var dx = v.x + (nodeR + 2) * Math.cos(da);
  363. var dy = v.y + (nodeR + 2) * Math.sin(da);
  364. ctx.beginPath();
  365. ctx.arc(dx, dy, 1.5, 0, Math.PI * 2);
  366. ctx.fillStyle = d.color + '66';
  367. ctx.fill();
  368. }
  369. }
  370. // ================================================================
  371. // 6. Center — Tai Chi overlay (handled by HTML/CSS SVG)
  372. // ================================================================
  373. // (The center emblem is replaced by the .taiji-center SVG overlay.)
  374. }
  375. // --------------------------------------------------------
  376. // 8. Knowledge Base — fetch featured articles from API
  377. // --------------------------------------------------------
  378. const API_BASE = 'https://cfc.etotem.com.cn';
  379. (function loadFeaturedArticles() {
  380. const grid = document.getElementById('knowledgeGrid');
  381. const loading = document.getElementById('knowledgeLoading');
  382. if (!grid) return;
  383. fetch(API_BASE + '/api/articles/featured', {
  384. method: 'POST',
  385. headers: { 'Content-Type': 'application/json' },
  386. body: JSON.stringify({ size: 4 })
  387. })
  388. .then(function (res) { return res.json(); })
  389. .then(function (result) {
  390. if (loading) loading.style.display = 'none';
  391. if (!result.data || !result.data.length) {
  392. grid.innerHTML = '<p class="knowledge-error">暂无内容,敬请期待</p>';
  393. return;
  394. }
  395. var html = '';
  396. var dimColors = {
  397. '身': '#FF8C42', '智': '#6366F1', '富': '#F59E0B',
  398. '行': '#10B981', '心': '#FF6B9D'
  399. };
  400. result.data.forEach(function (article) {
  401. var relatedDim = article.relatedDimensions || '';
  402. var dimTag = '';
  403. if (relatedDim && dimColors[relatedDim]) {
  404. dimTag = '<span class="dimension-tag" style="background:' + dimColors[relatedDim] + '">' + relatedDim + '</span>';
  405. } else if (relatedDim) {
  406. dimTag = '<span class="dimension-tag" style="background:#4A9BD7">' + relatedDim + '</span>';
  407. }
  408. var coverHtml = article.coverImage
  409. ? '<img class="knowledge-card-cover" src="' + API_BASE + article.coverImage + '" alt="' + article.title + '" loading="lazy">'
  410. : '<div class="knowledge-card-cover"><svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/><path d="M8 7h6M8 11h8M8 15h4"/></svg></div>';
  411. var catName = article.categoryName || '';
  412. html += '<div class="knowledge-card">'
  413. + coverHtml
  414. + '<div class="knowledge-card-body">'
  415. + (catName ? '<span class="knowledge-card-category">' + catName + '</span>' : '')
  416. + '<h3 class="knowledge-card-title">' + article.title + '</h3>'
  417. + (article.summary ? '<p class="knowledge-card-summary">' + article.summary + '</p>' : '')
  418. + '<div class="knowledge-card-meta">'
  419. + dimTag
  420. + (article.readTime ? '<span>阅读约 ' + article.readTime + ' 分钟</span>' : '')
  421. + '</div></div></div>';
  422. });
  423. grid.innerHTML = html;
  424. })
  425. .catch(function () {
  426. if (loading) loading.style.display = 'none';
  427. grid.innerHTML = '<p class="knowledge-error">暂时无法加载,请稍后再试</p>';
  428. });
  429. })();
  430. // --------------------------------------------------------
  431. // 9. Floating CTA toggle
  432. // --------------------------------------------------------
  433. const floatCtaBtn = document.getElementById('floatCtaBtn');
  434. const floatCtaPopup = document.getElementById('floatCtaPopup');
  435. const floatCtaClose = document.getElementById('floatCtaClose');
  436. if (floatCtaBtn && floatCtaPopup) {
  437. floatCtaBtn.addEventListener('click', (e) => {
  438. e.stopPropagation();
  439. floatCtaPopup.classList.toggle('open');
  440. });
  441. if (floatCtaClose) {
  442. floatCtaClose.addEventListener('click', (e) => {
  443. e.stopPropagation();
  444. floatCtaPopup.classList.remove('open');
  445. });
  446. }
  447. document.addEventListener('click', () => {
  448. floatCtaPopup.classList.remove('open');
  449. });
  450. floatCtaPopup.addEventListener('click', (e) => {
  451. e.stopPropagation();
  452. });
  453. }
  454. // --------------------------------------------------------
  455. // 9. Cookie consent banner
  456. // --------------------------------------------------------
  457. const cookieConsent = document.getElementById('cookieConsent');
  458. const cookieConsentBtn = document.getElementById('cookieConsentBtn');
  459. if (cookieConsent && cookieConsentBtn) {
  460. if (!localStorage.getItem('cookieConsent')) {
  461. setTimeout(() => cookieConsent.classList.add('visible'), 600);
  462. }
  463. cookieConsentBtn.addEventListener('click', () => {
  464. cookieConsent.classList.remove('visible');
  465. localStorage.setItem('cookieConsent', 'true');
  466. });
  467. }
  468. // Initial draw
  469. drawWuxingDiagram();
  470. // Redraw on orientation/resize via ResizeObserver
  471. if (window.ResizeObserver) {
  472. const wuxingContainer = document.querySelector('.wuxing-diagram');
  473. if (wuxingContainer) {
  474. const ro = new ResizeObserver(() => drawWuxingDiagram());
  475. ro.observe(wuxingContainer);
  476. }
  477. }
  478. });
  479. // --------------------------------------------------------
  480. // 9. User behavior analytics — Clarity custom events
  481. // --------------------------------------------------------
  482. (function() {
  483. var SCROLL_DEPTHS = [25, 50, 75, 90, 100];
  484. var reportedDepths = {};
  485. function trackScrollDepth() {
  486. var docHeight = document.documentElement.scrollHeight - window.innerHeight;
  487. if (docHeight <= 0) return;
  488. var scrolled = Math.round((window.scrollY / docHeight) * 100);
  489. SCROLL_DEPTHS.forEach(function(depth) {
  490. if (scrolled >= depth && !reportedDepths[depth]) {
  491. reportedDepths[depth] = true;
  492. if (window.clarity) {
  493. window.clarity('event', 'scroll_depth', { depth: depth + '%' });
  494. }
  495. }
  496. });
  497. }
  498. var scrollTick = false;
  499. window.addEventListener('scroll', function() {
  500. if (!scrollTick) {
  501. requestAnimationFrame(function() {
  502. trackScrollDepth();
  503. scrollTick = false;
  504. });
  505. scrollTick = true;
  506. }
  507. }, { passive: true });
  508. document.addEventListener('click', function(e) {
  509. var target = e.target.closest('a, button');
  510. if (!target) return;
  511. var eventData = {};
  512. if (target.tagName === 'A') {
  513. var href = target.getAttribute('href') || '';
  514. var text = (target.textContent || '').trim().slice(0, 40);
  515. if (href.startsWith('#')) {
  516. eventData = { type: 'anchor', target: href, text: text };
  517. } else if (href.startsWith('http') || href.startsWith('//')) {
  518. eventData = { type: 'outbound', target: href, text: text };
  519. } else {
  520. eventData = { type: 'internal', target: href, text: text };
  521. }
  522. if (target.getAttribute('target') === '_blank') {
  523. eventData.external = true;
  524. }
  525. }
  526. if (target.tagName === 'BUTTON') {
  527. eventData = { type: 'button', text: (target.textContent || '').trim().slice(0, 40) };
  528. }
  529. if (window.clarity && eventData.type) {
  530. window.clarity('event', 'cta_click', eventData);
  531. }
  532. });
  533. var DWELL_INTERVAL = 30000;
  534. var MAX_DWELL_BEATS = 10;
  535. var dwellCount = 0;
  536. var dwellTimer = setInterval(function() {
  537. dwellCount++;
  538. if (window.clarity) {
  539. window.clarity('event', 'dwell', { seconds: dwellCount * 30 });
  540. }
  541. if (dwellCount >= MAX_DWELL_BEATS) {
  542. clearInterval(dwellTimer);
  543. }
  544. }, DWELL_INTERVAL);
  545. if (window.clarity) {
  546. window.clarity('event', 'session_info', {
  547. width: screen.width,
  548. height: screen.height,
  549. referrer: document.referrer || '(direct)'
  550. });
  551. }
  552. })();