网址提交

This commit is contained in:
浅念
2026-08-27 11:54:26 +08:00
commit f4fb024e98
177 changed files with 9100 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
/* ============================================================
About Us V3 — NIO-style animations
文字/Logo = 触发式(到达阈值 → 播完)
蒙版/背景缩放 = scrub 式(随滚动持续变化)
============================================================ */
(function () {
'use strict';
var s1 = document.getElementById('au-1');
var s3 = document.getElementById('au-3');
if (!s1) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.documentElement.classList.add('rm');
return;
}
var pin1 = s1.closest('.au-pin');
var pin3 = s3 ? s3.closest('.au-pin') : null;
var mask1 = s1.querySelector('.au-mask');
var bg1 = s1.querySelector('.au-bg');
var mask3 = s3 ? s3.querySelector('.au-zoom-mask') : null;
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function check() {
var y = window.scrollY;
var vh = window.innerHeight;
/* ---- au-1 ---- */
// 文字:触发式(过 30% → 加 .play)
var t1 = pin1.offsetTop + vh * 0.3;
s1.classList.toggle('play', y > t1);
// 蒙版:scrub 式(随滚动持续加深,0 → 1 跨 1.5 屏)
var p1 = clamp((y - pin1.offsetTop) / (vh * 1.5), 0, 1);
mask1.style.opacity = p1.toFixed(3);
// 背景:scrub 式(随滚动持续微缩 1.04 → 1)
bg1.style.transform = 'scale(' + (1.04 - 0.04 * p1).toFixed(4) + ')';
/* ---- au-3 ---- */
if (pin3) {
// 文字/Logo:触发式
var t3 = pin3.offsetTop + vh * 1.0;
s3.classList.toggle('play', y > t3);
// 黑幕:scrub 式(随滚动持续加深,0 → 0.8 跨 1.5 屏)
if (mask3) {
var p3 = clamp((y - pin3.offsetTop - vh * 0.5) / (vh * 1.5), 0, 1);
mask3.style.opacity = (p3 * 0.8).toFixed(3);
}
}
}
var ticking = false;
window.addEventListener('scroll', function () {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () { check(); ticking = false; });
}, { passive: true });
window.addEventListener('resize', function () {
pin1 = s1.closest('.au-pin');
pin3 = s3 ? s3.closest('.au-pin') : null;
check();
}, { passive: true });
check();
})();
/* GPS carousel */
(function () {
'use strict';
var viewport = document.querySelector('.au-gps__viewport');
var track = document.querySelector('.au-gps__track');
var dotsWrap = document.querySelector('.au-gps__dots');
if (!viewport || !track || !dotsWrap) return;
var cards = Array.prototype.slice.call(track.children);
if (cards.length < 2) return;
var idx = 0, timer = null;
cards.forEach(function (_, i) {
var b = document.createElement('button');
b.setAttribute('aria-label', 'Slide ' + (i + 1));
b.addEventListener('click', function () { goTo(i); restart(); });
dotsWrap.appendChild(b);
});
var dots = Array.prototype.slice.call(dotsWrap.children);
function render() {
track.style.transform = 'translateX(' + (-idx * 100) + '%)';
dots.forEach(function (d, i) { d.classList.toggle('active', i === idx); });
}
function goTo(i) { idx = ((i % cards.length) + cards.length) % cards.length; render(); }
function next() { goTo(idx + 1); }
function start() { if (!timer) timer = setInterval(next, 4000); }
function stop() { clearInterval(timer); timer = null; }
function restart(){ stop(); start(); }
render(); start();
viewport.addEventListener('mouseenter', stop);
viewport.addEventListener('mouseleave', start);
document.addEventListener('visibilitychange', function () {
document.hidden ? stop() : start();
});
var x0 = null;
viewport.addEventListener('pointerdown', function (e) { x0 = e.clientX; });
window.addEventListener('pointerup', function (e) {
if (x0 === null) return;
var dx = e.clientX - x0; x0 = null;
if (Math.abs(dx) > 40) { dx < 0 ? next() : goTo(idx - 1); restart(); }
});
})();
+119
View File
@@ -0,0 +1,119 @@
/* ============================================================
About Us — CSS sticky pin + rAF scroll-linked (no GSAP)
Smoothest approach: CSS position:sticky handles the pin
(native GPU compositing), rAF drives the cross-fade timeline.
Stage 1 (au-1): intro(screen1) -> slogan(screen2)
Stage 2 (au-3): Strong Resources(screen3) -> zoom+logo(screen4)
============================================================ */
(function () {
var s1 = document.querySelector('#au-1');
var s3 = document.querySelector('#au-3');
if (!s1) return;
var bg = s1.querySelector('.au-bg');
var intro = s1.querySelector('.au-inner');
var corner = s1.querySelector('.au-corner-img');
var mask = s1.querySelector('.au-mask');
var layer = s1.querySelector('.au-slogan-layer');
var sl = s1.querySelector('.au-slogan');
var cn = s1.querySelector('.au-slogan-cn');
var head = s3 ? s3.querySelector('.au-sec-head') : null;
var zoom = s3 ? s3.querySelector('.au-zoom-img') : null;
var zoomMask = s3 ? s3.querySelector('.au-zoom-mask') : null;
var reveal = s3 ? s3.querySelector('.au-reveal') : null;
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function lerp(a, b, t) { return a + (b - a) * clamp(t, 0, 1); }
var start1 = 0, start3 = 0;
function measure() {
function dt(el) { var r = el.getBoundingClientRect(); return r.top + window.scrollY; }
start1 = dt(s1);
if (s3) start3 = dt(s3);
}
function update() {
var vh = window.innerHeight;
/* ---- au-1 (pin 2vh): intro held -> cross-fade to slogan ---- */
var p = clamp((window.scrollY - start1) / vh, 0, 2); // 0 -> 2
// bg: subtle cinematic scale throughout
if (bg) bg.style.transform = 'scale(' + lerp(1.04, 1, p / 2).toFixed(4) + ')';
// intro (screen 1): held 0-0.3, fades up 0.3-0.6
var introT = clamp((p - 0.3) / 0.3, 0, 1);
if (intro) { intro.style.opacity = String(1 - introT); intro.style.transform = 'translateY(' + (-introT * 30).toFixed(1) + 'px)'; }
if (corner) corner.style.opacity = String(1 - introT);
// mask: darkens 0.3-0.6
if (mask) mask.style.opacity = String(introT);
// slogan (screen 2): triggered CSS transition at threshold (NIO-style auto rise-up)
if (layer) layer.classList.toggle('is-current', p >= 0.5);
/* ---- au-3 (pin 2vh): Strong Resources -> zoom + logo ---- */
if (s3) {
var q = clamp((window.scrollY - start3) / vh, 0, 2);
var t = clamp((q - 1.0) / 1.0, 0, 1);
// head (screen 3): held first screen, fades second
if (head) head.style.opacity = String(clamp(1 - (q - 0.6) / 0.3, 0, 1));
// zoom: scale to fullscreen; photo opacity stays 1 — a black veil fades in over it
if (zoom) {
zoom.style.transform = 'translate(-50%, -50%) scale(' + lerp(1, 2.2, t).toFixed(3) + ')';
}
if (zoomMask) zoomMask.style.opacity = String(lerp(0, 0.8, t).toFixed(3));
// reveal (screen 4): logo+text in second screen
if (reveal) reveal.style.opacity = String(clamp((q - 1.1) / 0.4, 0, 1));
}
}
var ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () { update(); ticking = false; });
}
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', function () { measure(); onScroll(); });
measure();
update();
})();
/* ============================================================
GPS data carousel — auto-rotate + dot navigation
============================================================ */
(function () {
var track = document.querySelector('.au-gps__track');
var dotsWrap = document.querySelector('.au-gps__dots');
if (!track || !dotsWrap) return;
var cards = Array.prototype.slice.call(track.children);
if (cards.length < 2) return;
var idx = 0, timer = null;
cards.forEach(function (_, i) {
var b = document.createElement('button');
b.setAttribute('aria-label', 'Slide ' + (i + 1));
b.addEventListener('click', function () { goTo(i); restart(); });
dotsWrap.appendChild(b);
});
var dots = Array.prototype.slice.call(dotsWrap.children);
function render() {
track.style.transform = 'translateX(' + (-idx * 100) + '%)';
dots.forEach(function (d, i) { d.classList.toggle('active', i === idx); });
}
function goTo(i) { idx = ((i % cards.length) + cards.length) % cards.length; render(); }
function next() { goTo(idx + 1); }
function start() { timer = setInterval(next, 4000); }
function restart() { clearInterval(timer); start(); }
render();
start();
// pause auto-rotate while hovering
track.addEventListener('mouseenter', function () { clearInterval(timer); });
track.addEventListener('mouseleave', start);
})();
+35
View File
@@ -0,0 +1,35 @@
/* ============================================================
HESU — Contact Us page (vanilla JS, no dependencies)
· inquiry form: client-side validation + success state
(static site — no backend, form does not send anywhere yet)
· scroll reveal is handled by js/main.js (unified)
============================================================ */
(function () {
'use strict';
var form = document.getElementById('ctForm');
var note = document.getElementById('ctNote');
if (!form || !note) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!form.checkValidity()) {
form.classList.add('was-validated');
var bad = form.querySelector(':invalid');
if (bad) bad.focus();
note.textContent = 'Please fill in the required fields marked with *.';
note.className = 'ct-note is-err';
return;
}
form.classList.remove('was-validated');
form.reset();
note.textContent = 'Thank you for your inquiry. For a quicker response you can also reach us via TEL/WhatsApp: +8613526635577.';
note.className = 'ct-note is-ok';
});
form.addEventListener('input', function () {
if (note.textContent) { note.textContent = ''; note.className = 'ct-note'; }
});
})();
+90
View File
@@ -0,0 +1,90 @@
/* ============================================================
Global Presence — fullpage screen controller
One screen per wheel gesture / swipe / arrow; dot nav jumps.
Footer (after the last screen) remains freely reachable.
============================================================ */
(function () {
var screens = Array.prototype.slice.call(document.querySelectorAll('.gp-screen'));
var links = Array.prototype.slice.call(document.querySelectorAll('.gp-nav-list a'));
var n = screens.length;
if (!n) return;
var current = 0;
var animating = false;
function topOf(i) { return screens[i].offsetTop; }
function clamp(i) { return Math.max(0, Math.min(n - 1, i)); }
function setActive(i) {
current = i;
links.forEach(function (a) {
a.classList.toggle('active', parseInt(a.getAttribute('data-i'), 10) === i);
});
}
function go(i) {
i = clamp(i);
if (animating || i === current) return;
animating = true;
window.scrollTo({ top: topOf(i), behavior: 'smooth' });
setTimeout(function () { animating = false; }, 650);
}
function syncActive() {
var y = window.scrollY + window.innerHeight * 0.4;
var i = 0;
for (var k = 0; k < n; k++) { if (y >= topOf(k)) i = k; }
if (i !== current) setActive(i);
}
function inFooterZone() { return window.scrollY > topOf(n - 1) + 20; }
// ---- wheel: one screen per gesture; release to footer at the last screen ----
window.addEventListener('wheel', function (e) {
if (animating) { e.preventDefault(); return; }
var d = e.deltaY;
if (inFooterZone()) {
// free scroll inside footer; snap back up when crossing the boundary
if (d < 0 && window.scrollY <= topOf(n - 1) + 80) { e.preventDefault(); go(n - 1); }
return;
}
if (current === n - 1 && d > 0) return; // allow scrolling down into the footer
e.preventDefault();
if (Math.abs(d) < 5) return;
go(current + (d > 0 ? 1 : -1));
}, { passive: false });
// ---- keyboard ----
window.addEventListener('keydown', function (e) {
var k = e.key;
if (k === 'ArrowDown' || k === 'PageDown') { e.preventDefault(); go(current + 1); }
else if (k === 'ArrowUp' || k === 'PageUp') { e.preventDefault(); go(current - 1); }
else if (k === 'Home') { e.preventDefault(); go(0); }
else if (k === 'End') { e.preventDefault(); go(n - 1); }
});
// ---- touch swipe ----
var ty = null;
window.addEventListener('touchstart', function (e) { ty = e.touches[0].clientY; }, { passive: true });
window.addEventListener('touchend', function (e) {
if (ty === null) return;
var dy = ty - e.changedTouches[0].clientY;
if (Math.abs(dy) > 50) go(current + (dy > 0 ? 1 : -1));
ty = null;
}, { passive: true });
// ---- region nav links (overview screen) ----
links.forEach(function (a) {
a.addEventListener('click', function (e) {
e.preventDefault();
var i = parseInt(a.getAttribute('data-i'), 10);
if (!isNaN(i)) go(i);
});
});
// ---- sync + resize ----
window.addEventListener('scroll', syncActive, { passive: true });
window.addEventListener('resize', function () { window.scrollTo({ top: topOf(current), behavior: 'auto' }); });
syncActive();
})();
+458
View File
@@ -0,0 +1,458 @@
/* ============================================================
HESU — homepage interactions (vanilla JS, no dependencies)
============================================================ */
(function () {
'use strict';
/* ---------- header scrolled state ---------- */
var header = document.getElementById('siteHeader');
function onScroll() {
if (!header) return;
if (window.scrollY > 40) header.classList.add('scrolled');
else header.classList.remove('scrolled');
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
/* ---------- mobile drawer ---------- */
var toggle = document.getElementById('menuToggle');
var nav = document.getElementById('mainNav');
// backdrop
var backdrop = document.createElement('div');
backdrop.className = 'nav-backdrop';
document.body.appendChild(backdrop);
function closeNav() {
if (!nav || !toggle) return;
nav.classList.remove('open');
toggle.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
backdrop.classList.remove('show');
document.body.style.overflow = '';
}
function openNav() {
nav.classList.add('open');
toggle.classList.add('open');
toggle.setAttribute('aria-expanded', 'true');
backdrop.classList.add('show');
document.body.style.overflow = 'hidden';
}
if (toggle && nav) {
toggle.addEventListener('click', function () {
nav.classList.contains('open') ? closeNav() : openNav();
});
}
backdrop.addEventListener('click', closeNav);
// close on link click (mobile)
if (nav) {
nav.querySelectorAll('a').forEach(function (a) {
a.addEventListener('click', function () {
if (window.innerWidth <= 860) {
// allow mega parent toggle to work first on mobile
if (a.parentElement.classList.contains('has-mega')) return;
closeNav();
}
});
});
/* ---------- mobile dropdown collapsibles ---------- */
nav.querySelectorAll('.nav-item.has-mega > a').forEach(function (trigger) {
trigger.addEventListener('click', function (e) {
if (window.innerWidth > 860) return;
e.preventDefault();
var li = trigger.parentElement;
// close other open items
nav.querySelectorAll('.nav-item.has-mega.open').forEach(function (other) {
if (other !== li) other.classList.remove('open');
});
li.classList.toggle('open');
});
});
}
/* ---------- Full-screen mega menus (desktop hover; mobile uses drawer collapsible) ---------- */
(function () {
var items = Array.prototype.slice.call(document.querySelectorAll('.nav-item.has-mega'));
if (!items.length) return;
var openTimer, closeTimer;
function closeAll() {
document.querySelectorAll('.mega.open').forEach(function (m) { m.classList.remove('open'); });
items.forEach(function (it) { it.classList.remove('mega-on'); });
}
function open(mega, item) {
closeAll();
mega.classList.add('open');
item.classList.add('mega-on');
}
items.forEach(function (item) {
var mega = item.querySelector('.mega');
if (!mega) return;
item.addEventListener('mouseenter', function () {
if (window.innerWidth <= 860) return;
clearTimeout(closeTimer);
openTimer = setTimeout(function () { open(mega, item); }, 120);
});
item.addEventListener('mouseleave', function () {
if (window.innerWidth <= 860) return;
clearTimeout(openTimer);
closeTimer = setTimeout(closeAll, 200);
});
mega.querySelectorAll('.mega-card').forEach(function (c) {
c.addEventListener('click', function () { closeAll(); });
});
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(); });
})();
/* ---------- partner carousel ---------- */
var carousel = document.getElementById('partnerCarousel');
var dotsWrap = document.getElementById('partnerDots');
if (carousel && dotsWrap) {
var slides = Array.prototype.slice.call(carousel.querySelectorAll('.slide'));
var idx = 0;
var timer = null;
slides.forEach(function (_, i) {
var b = document.createElement('button');
b.setAttribute('role', 'tab');
b.setAttribute('aria-label', 'Slide ' + (i + 1));
b.addEventListener('click', function () { goTo(i, true); });
dotsWrap.appendChild(b);
});
var dots = Array.prototype.slice.call(dotsWrap.children);
function render() {
slides.forEach(function (s, i) { s.classList.toggle('is-active', i === idx); });
dots.forEach(function (d, i) { d.classList.toggle('active', i === idx); });
}
function goTo(i, user) {
idx = (i + slides.length) % slides.length;
render();
if (user) restart();
}
function next() { goTo(idx + 1); }
function stop() { clearInterval(timer); timer = null; }
function start() { stop(); timer = setInterval(next, 5000); }
function restart() { start(); }
render();
start();
carousel.addEventListener('mouseenter', stop);
carousel.addEventListener('mouseleave', start);
}
/* ---------- scroll reveal ---------- */
var reveals = document.querySelectorAll('[data-reveal], .reveal'); /* unified reveal — page scripts removed */
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
/* reveal-repeat: replay the stagger every time the element enters the
viewport (reset while fully out) — used by the bento business cells */
if (en.target.classList.contains('reveal-repeat')) {
en.target.classList.toggle('in', en.isIntersecting);
return;
}
if (en.isIntersecting) {
en.target.classList.add('in');
io.unobserve(en.target);
}
});
}, { threshold: 0.12 });
reveals.forEach(function (el) { io.observe(el); });
} else {
reveals.forEach(function (el) { el.classList.add('in'); });
}
/* ---------- stats count-up ---------- */
function animateCount(el) {
var target = parseInt(el.getAttribute('data-count'), 10);
if (isNaN(target)) return;
var plus = el.querySelector('.plus');
var dur = 1400, start = null;
function step(ts) {
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3); // easeOutCubic
var val = Math.floor(eased * target);
el.firstChild.nodeValue = val.toLocaleString();
if (p < 1) requestAnimationFrame(step);
else el.firstChild.nodeValue = target.toLocaleString();
}
requestAnimationFrame(step);
}
var stats = document.querySelectorAll('.stat-num');
if ('IntersectionObserver' in window) {
var so = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) {
animateCount(en.target);
so.unobserve(en.target);
}
});
}, { threshold: 0.4 });
stats.forEach(function (el) { so.observe(el); });
}
/* ---------- Network peek carousel — drag to slide, seamless infinite loop (same drag system on desktop & mobile) ---------- */
(function () {
var stage = document.querySelector('.network-stage');
if (!stage) return;
var strip = stage.querySelector('.network-strip');
var track = stage.querySelector('.network-track');
if (!track) return;
var realCards = Array.prototype.slice.call(track.querySelectorAll('.network-card'));
var n = realCards.length;
if (!n) return;
var GAP = 18, VISIBLE = 3.6, PAIRED = true; // desktop defaults; mobile re-tunes in applyMode
var pos = n; // continuous position (left card of centered pair)
var cardW = 0, cardStep = 1, stripW = 0, centerBase = 0;
var cloned = false, enabled = false;
// build [clone][real][clone] so both edges always have cards (no blank when looping)
function setClones(on) {
if (on && !cloned) {
var pre = document.createDocumentFragment();
var post = document.createDocumentFragment();
realCards.forEach(function (c) {
var a = c.cloneNode(true); a.setAttribute('data-clone', ''); pre.appendChild(a);
var b = c.cloneNode(true); b.setAttribute('data-clone', ''); post.appendChild(b);
});
track.insertBefore(pre, track.firstChild);
track.appendChild(post);
cloned = true;
} else if (!on && cloned) {
Array.prototype.slice.call(track.querySelectorAll('.network-card[data-clone]')).forEach(function (c) { c.remove(); });
cloned = false;
}
}
function measure() {
stripW = strip.getBoundingClientRect().width;
cardW = stripW / VISIBLE;
cardStep = cardW + GAP;
centerBase = PAIRED ? (stripW - 2 * cardW - GAP) / 2 : (stripW - cardW) / 2;
var all = track.querySelectorAll('.network-card');
for (var i = 0; i < all.length; i++) all[i].style.flexBasis = cardW + 'px';
}
function transformFor(p) { return 'translateX(' + (centerBase - p * cardStep) + 'px)'; }
function render(animate) {
track.style.transition = animate ? '' : 'none';
track.style.transform = transformFor(pos);
}
function normalize() { pos = n + (((pos - n) % n) + n) % n; } // wrap into real band [n, 2n)
// after a slide settles, silently fold position back into the real band (seamless)
track.addEventListener('transitionend', function (e) {
if (enabled && e.propertyName === 'transform') { normalize(); render(false); }
});
// drag to slide (pointer = mouse + touch)
var dragging = false, startX = 0, startP = 0, delta = 0, moved = false;
function onDown(e) {
if (!enabled) return;
dragging = true; moved = false; delta = 0;
startX = e.clientX; startP = pos;
track.classList.add('dragging');
}
function onMove(e) {
if (!dragging) return;
delta = e.clientX - startX;
if (Math.abs(delta) > 6) moved = true;
track.style.transform = transformFor(startP - delta / cardStep);
}
function onUp() {
if (!dragging) return;
dragging = false;
track.classList.remove('dragging');
pos = Math.round(startP - delta / cardStep); // snap to nearest card
render(true); // animate; transitionend folds it back
}
strip.addEventListener('pointerdown', onDown);
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
window.addEventListener('pointercancel', onUp);
// suppress link navigation right after a real drag
track.addEventListener('click', function (e) { if (moved) { e.preventDefault(); e.stopPropagation(); } }, true);
function applyMode() {
var mobile = window.matchMedia('(max-width: 860px)').matches;
PAIRED = !mobile; // desktop centers a card pair; mobile centers one card
GAP = mobile ? 12 : 18;
VISIBLE = mobile ? 1.32 : 3.6; // mobile: centered card + neighbour peeks
enabled = true;
setClones(true);
measure();
if (!track.dataset.init) { pos = n; track.dataset.init = '1'; }
if (pos < 1 || pos >= 2 * n) pos = n;
render(false);
}
var rT;
window.addEventListener('resize', function () { clearTimeout(rT); rT = setTimeout(applyMode, 150); });
applyMode();
})();
/* ---------- presence map — bloom all, then rotating-window highlights ----------
Entering the viewport blooms ALL locations at once; the cycle then runs the
sliding window straight from its steady state: every tick the next location
replays its full entrance while the oldest visible one exits (wrapping around,
the last location returns exactly as the first one leaves). Four stay visible.
Leaving the viewport gracefully exits everything; coming back blooms again. */
(function () {
var overlay = document.querySelector('.map-overlay');
if (!overlay) return;
var groups = Array.prototype.slice.call(overlay.querySelectorAll('.location'));
if (!groups.length) return;
function show(i) {
groups.forEach(function (g, j) { g.classList.toggle('active', j === i); });
}
// drop the DUBAI active baked into the HTML (no-JS keeps it as a static fallback)
show(-1);
var tick = 0, timer = null, run = 0;
// stride-2 visit order (0,2,4,1,3) so the point entering and the point
// exiting each step are far apart on the map instead of neighbours
var order = [];
var stride = groups.length % 2 === 1 ? 2 : (groups.length > 3 ? 3 : 1);
for (var i = 0; i < groups.length; i++) order.push((i * stride) % groups.length);
// deactivate with the reverse (.out) choreography — class is lifted when it ends
function exit(el) {
el.classList.remove('active');
if (el.classList.contains('out')) return; // already on its way out
el.classList.add('out');
setTimeout(function () { el.classList.remove('out'); }, 650);
}
function step() {
var on = order[tick % order.length];
var off = order[(tick + 1) % order.length];
groups.forEach(function (g, i) {
if (i === on) {
// remove → flush → re-add so the entrance sequence replays
g.classList.remove('active');
g.getBoundingClientRect();
g.classList.remove('out');
g.classList.add('active');
} else if (i === off) {
exit(g);
}
});
tick++;
}
function start() {
if (timer) return;
var myRun = ++run;
// let one frame of the invisible base state paint first, then bloom everything
requestAnimationFrame(function () {
requestAnimationFrame(function () {
if (run !== myRun) return;
tick = 0;
groups.forEach(function (g) { g.classList.remove('out'); g.classList.add('active'); });
timer = setInterval(step, 2000);
});
});
timer = 1; // mark as starting (replaced by the real interval above)
}
function stop() {
if (timer) { clearInterval(timer); timer = null; }
run++; // invalidate pending callbacks
tick = 0;
groups.forEach(function (g) { if (g.classList.contains('active')) exit(g); });
}
var wrap = document.querySelector('.map-wrap');
if ('IntersectionObserver' in window && wrap) {
var mo = new IntersectionObserver(function (entries) {
entries[0].isIntersecting ? start() : stop();
}, { threshold: 0.12 });
mo.observe(wrap);
} else {
start();
}
})();
/* ---------- ae page map — bloom all, then rotating-window highlights ----------
Same logic as the homepage map: enter blooms everything, then the sliding
window runs from its steady state (next marker replays in, oldest exits). */
(function () {
var overlay = document.querySelector('.ae-map__overlay');
if (!overlay) return;
var markers = Array.prototype.slice.call(overlay.querySelectorAll('.marker'));
if (!markers.length) return;
function show(i) {
markers.forEach(function (m, j) { m.classList.toggle('active', j === i); });
}
// drop the Belarus active baked into the HTML (no-JS keeps it as a static fallback)
show(-1);
var tick = 0, timer = null, run = 0;
// stride-2 visit order so the marker entering and the marker exiting each
// step are far apart on the map instead of neighbours
var order = [];
var stride = markers.length % 2 === 1 ? 2 : (markers.length > 3 ? 3 : 1);
for (var i = 0; i < markers.length; i++) order.push((i * stride) % markers.length);
// deactivate with the reverse (.out) choreography — class is lifted when it ends
function exit(el) {
el.classList.remove('active');
if (el.classList.contains('out')) return; // already on its way out
el.classList.add('out');
setTimeout(function () { el.classList.remove('out'); }, 650);
}
function step() {
var on = order[tick % order.length];
var off = order[(tick + 1) % order.length];
markers.forEach(function (m, i) {
if (i === on) {
// remove → flush → re-add so the entrance sequence replays
m.classList.remove('active');
m.getBoundingClientRect();
m.classList.remove('out');
m.classList.add('active');
} else if (i === off) {
exit(m);
}
});
tick++;
}
function start() {
if (timer) return;
var myRun = ++run;
requestAnimationFrame(function () {
requestAnimationFrame(function () {
if (run !== myRun) return;
tick = 0;
markers.forEach(function (m) { m.classList.remove('out'); m.classList.add('active'); });
timer = setInterval(step, 1800);
});
});
timer = 1;
}
function stop() {
if (timer) { clearInterval(timer); timer = null; }
run++; // invalidate pending callbacks
tick = 0;
markers.forEach(function (m) { if (m.classList.contains('active')) exit(m); });
}
var wrap = overlay.closest('.ae-map');
if ('IntersectionObserver' in window && wrap) {
var mo = new IntersectionObserver(function (entries) {
entries[0].isIntersecting ? start() : stop();
}, { threshold: 0.12 });
mo.observe(wrap);
} else {
start();
}
})();
/* ---------- footer year ---------- */
var y = document.getElementById('year');
if (y) y.textContent = new Date().getFullYear();
})();
+106
View File
@@ -0,0 +1,106 @@
/* ============================================================
HESU — News detail page (vanilla JS, no dependencies)
One page serves all articles: news-detail.html?id=N
Renders meta / title / text-image blocks / prev-next.
blocks: { t:'p', text:'…' } | { t:'img', src:'…' }
============================================================ */
(function () {
'use strict';
/* ---------- article content (id matches news.js href ?id=N) ---------- */
var ARTICLES = [
{ id: 1, cat: 'Company News', date: '2025.04.15',
title: 'Hesu Industrial Participates in the 139th Canton Fair',
blocks: [
{ t: 'p', text: 'The 139th China Import and Export Fair (Canton Fair) was successfully held in Guangzhou. As a key enterprise in the foreign trade industry, Hesu Industrial organized a professional team to attend the exhibition. Taking the opportunity of the Canton Fair’s global business platform, the company fully displayed its core products, customized production solutions and one-stop foreign trade service system, demonstrating its strong manufacturing strength, reliable product quality and global service capabilities.' },
{ t: 'img', src: 'image/news/detiles1-01.jpg' },
{ t: 'p', text: 'As an important window for the company to expand the international market, the 139th Canton Fair has further expanded Hesu Industrial’s global customer channels and consolidated the foundation of overseas market layout. In the future, adhering to professional manufacturing advantages, the company will seize foreign trade development opportunities, meet global customer needs with high-quality products and considerate services, and steadily increase its share in the international market.' },
{ t: 'img', src: 'image/news/detiles1-02.jpg' }
] },
{ id: 2, cat: 'Company News', date: '2025.03.02',
title: 'Hesu Industrial Dubai Branch Officially Opens for Business',
blocks: [
{ t: 'p', text: 'Recently, Hesu Industrial Dubai Branch was officially put into operation, marking the official implementation of the company’s localized development strategy in the Middle East and the further improvement of its global overseas service network.' },
{ t: 'img', src: 'image/news/detiles2-01.jpg' },
{ t: 'p', text: 'Dubai is located at the core business hub of Asia, Europe and Africa, with superior geographical advantages, serving as an important transit center radiating the Gulf countries, West Asia and North Africa markets. Integrating local office, product warehousing, customer reception and one-stop pre-sales and after-sales services, the Dubai branch has established a localized operation team. It can provide local customers with efficient services including sample docking, order performance, fast delivery and on-site technical support, effectively shortening the supply cycle, reducing cross-border trade costs and quickly responding to local market demands.' },
{ t: 'img', src: 'image/news/detiles2-02.jpg' },
{ t: 'p', text: 'The opening of the Dubai branch has built a complete overseas operation model of "domestic production + overseas warehousing and distribution + localized services" for Hesu Industrial. Going forward, the company will take Dubai as the core base to deeply cultivate the Middle East market, continuously optimize the regional supply chain, and carry out long-term and win-win business cooperation with local partners.' }
] },
{ id: 3, cat: 'Company News', date: '2025.02.10',
title: 'Hesu Industrial West Africa Factory Officially Put into Use',
blocks: [
{ t: 'p', text: 'After systematic planning and construction, equipment installation, commissioning and trial production, Hesu Industrial’s self-built production factory in West Africa has been officially completed and put into formal operation, becoming a core manufacturing base for the company to take root in the African market.' },
{ t: 'img', src: 'image/news/detiles3-01.jpg' },
{ t: 'p', text: 'Equipped with standardized fully automatic production lines, the West Africa factory strictly follows the unified production technology and quality control standards of the headquarters. It mainly realizes localized production and regional distribution for markets in West Africa and surrounding regions. The local factory effectively solves the problems of long cross-border shipping cycles and high tariff costs, greatly improves regional supply efficiency, and actively absorbs local labor employment. By adhering to the localized operation concept, the factory accurately meets the market demand for infrastructure and building materials in West Africa.' },
{ t: 'img', src: 'image/news/detiles3-02.jpg' },
{ t: 'p', text: 'From domestic production bases and Dubai overseas branches to local manufacturing bases in West Africa, Hesu Industrial has gradually built a global production and marketing network covering the Middle East and Africa. The official operation of the West Africa factory will further release the company’s overseas production capacity, enhance core competitiveness in the African market, and help high-quality Chinese manufacturing products take deeper root in West Africa.' }
] },
{ id: 4, cat: 'Company News', date: '2025.01.18',
title: 'Global Customers Visit Hesu Industrial Headquarters in China',
blocks: [
{ t: 'p', text: 'Recently, long-term cooperative customers from the Middle East and West Africa visited the headquarters of Hesu Industrial in China for on-site investigation and exchange. The two sides conducted in-depth business discussions to summarize previous cooperation achievements and clarify the development direction of long-term strategic cooperation.' },
{ t: 'img', src: 'image/news/detiles4-01.jpg' },
{ t: 'p', text: 'The visiting customers successively inspected the company’s production workshops, raw material warehouses, quality inspection laboratories and product exhibition halls. They had a comprehensive understanding of the whole production process, strict quality inspection standards and new product R&D strength, and spoke highly of the company’s standardized production management and large-scale production capacity. During the forum, both sides fully communicated on key issues such as customized product iteration, bulk order scheduling, exclusive regional agency and after-sales technical support, confirmed the annual framework cooperation plan, and reached a number of consensus on new product localization adaptation.' },
{ t: 'img', src: 'image/news/detiles4-02.jpg' },
{ t: 'p', text: 'The in-person visit of overseas customers fully reflects the high recognition and trust of global partners in Hesu Industrial’s brand strength and product quality. In the future, Hesu Industrial will always adhere to quality-oriented development, maintain open and win-win cooperation concepts, continuously optimize the global production and service system, and create stable and long-term value for all global partners.' }
] }
];
function byId(id) {
for (var i = 0; i < ARTICLES.length; i++) {
if (ARTICLES[i].id === id) return ARTICLES[i];
}
return null;
}
/* ---------- resolve ?id= (fallback: first article) ---------- */
var match = window.location.search.match(/[?&]id=(\d+)/);
var id = match ? parseInt(match[1], 10) : ARTICLES[0].id;
var a = byId(id) || ARTICLES[0];
var idx = ARTICLES.indexOf(a);
var prev = ARTICLES[idx - 1] || null;
var next = ARTICLES[idx + 1] || null;
/* ---------- render ---------- */
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
document.title = 'HESU — ' + a.title;
var catEl = document.getElementById('ndCat');
var dateEl = document.getElementById('ndDate');
var titleEl = document.getElementById('ndTitle');
var artEl = document.getElementById('ndArticle');
var navEl = document.getElementById('ndNav');
if (catEl) catEl.textContent = a.cat;
if (dateEl) dateEl.textContent = a.date;
if (titleEl) titleEl.textContent = a.title;
if (artEl) {
artEl.innerHTML = a.blocks.map(function (b) {
if (b.t === 'img') {
return '<figure class="nd-figure"><img loading=\"lazy\" src="' + esc(b.src) + '" alt="' + esc(a.title) + '" loading="lazy" /></figure>';
}
return '<p>' + esc(b.text) + '</p>';
}).join('');
}
if (navEl) {
var html = '';
if (prev) {
html += '<a href="news-detail.html?id=' + prev.id + '" class="nd-nav__item nd-nav__prev">' +
'<span class="nd-nav__label">Previous</span>' +
'<span class="nd-nav__title">' + esc(prev.title) + '</span></a>';
}
if (next) {
html += '<a href="news-detail.html?id=' + next.id + '" class="nd-nav__item nd-nav__next">' +
'<span class="nd-nav__label">Next</span>' +
'<span class="nd-nav__title">' + esc(next.title) + '</span></a>';
}
navEl.innerHTML = html;
}
})();
+170
View File
@@ -0,0 +1,170 @@
/* ============================================================
HESU — News & Insights listing (vanilla JS, no dependencies)
· category tabs (Company News / Delivery Cases / Exhibition Info)
· renders one row per article: text-left + image-right
· client-side pagination (5 per page) over the filtered set
· staggered scroll-reveal for rows + [data-reveal] elements
============================================================ */
(function () {
'use strict';
/* ---------- article data (newest first; cat matches a tab) ----------
Only Company News has real content for now; the other categories
are intentionally empty and show a "coming soon" state. ---------- */
var ARTICLES = [
{ cat: 'Company News', date: '2025.04.15', title: 'Hesu Industrial Participates in the 139th Canton Fair',
excerpt: 'The 139th China Import and Export Fair (Canton Fair) was successfully held in Guangzhou. As a key enterprise in the foreign trade industry, Hesu Industrial organized a professional team to attend the exhibition.',
img: 'image/news/news1.jpg', href: 'news-detail.html?id=1' },
{ cat: 'Company News', date: '2025.03.02', title: 'Hesu Industrial Dubai Branch Officially Opens for Business',
excerpt: 'Dubai is located at the core business hub of Asia, Europe and Africa, with superior geographical advantages, serving as an important transit center radiating the Gulf countries, West Asia and North Africa markets. Integrating local office',
img: 'image/news/news2.jpg', href: 'news-detail.html?id=2' },
{ cat: 'Company News', date: '2025.02.10', title: 'Hesu Industrial West Africa Factory Officially Put into Use',
excerpt: 'Equipped with standardized fully automatic production lines, the West Africa factory strictly follows the unified production technology and quality control standards of the headquarters.',
img: 'image/news/news3.jpg', href: 'news-detail.html?id=3' },
{ cat: 'Company News', date: '2025.01.18', title: 'Global Customers Visit Hesu Industrial Headquarters in China',
excerpt: 'The visiting customers successively inspected the company’s production workshops, raw material warehouses, quality inspection laboratories and product exhibition halls. They had a comprehensive understanding',
img: 'image/news/news4.jpg', href: 'news-detail.html?id=4' }
];
var PER_PAGE = 5;
var page = 1;
var currentCat = 'Company News'; /* default active tab */
var grid = document.getElementById('newsGrid');
var pager = document.getElementById('newsPagination');
var tabsWrap = document.getElementById('newsTabs');
if (!grid || !pager || !ARTICLES.length) return;
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function filtered() {
return ARTICLES.filter(function (a) { return a.cat === currentCat; });
}
function totalPages() {
return Math.max(1, Math.ceil(filtered().length / PER_PAGE));
}
/* ---------- row markup: text left + image right ---------- */
function rowHTML(a) {
var href = a.href || '#';
return '' +
'<article class="nw-row">' +
'<div class="nw-row__body">' +
'<h3 class="nw-row__title">' + esc(a.title) + '</h3>' +
'<p class="nw-row__excerpt">' + esc(a.excerpt) + '</p>' +
'<a class="nw-row__more" href="' + href + '">Explore More </a>' +
'</div>' +
'<a class="nw-row__media" href="' + href + '" aria-label="' + esc(a.title) + '" tabindex="-1">' +
'<img src="' + a.img + '" alt="' + esc(a.title) + '" loading="lazy" />' +
'</a>' +
'</article>';
}
function revealRows() {
var rows = grid.querySelectorAll('.nw-row');
if (!('IntersectionObserver' in window)) {
rows.forEach(function (r) { r.classList.add('in'); });
return;
}
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); }
});
}, { threshold: 0.1 });
rows.forEach(function (r, i) {
r.style.transitionDelay = ((i % PER_PAGE) * 0.07) + 's';
io.observe(r);
});
}
function renderGrid() {
var data = filtered();
if (!data.length) {
grid.innerHTML = '<div class="nw-empty">Coming soon — no ' + esc(currentCat) + ' yet.</div>';
pager.style.display = 'none';
return;
}
pager.style.display = '';
var start = (page - 1) * PER_PAGE;
grid.innerHTML = data.slice(start, start + PER_PAGE).map(rowHTML).join('');
revealRows();
}
/* ---------- pagination ---------- */
function makeButton(label, opts) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'nw-page' + (opts.nav ? ' nw-page--nav' : '') + (opts.active ? ' is-active' : '');
b.innerHTML = label;
if (opts.active) { b.setAttribute('aria-current', 'page'); b.disabled = true; }
if (opts.disabled) b.disabled = true;
if (typeof opts.go === 'number' && !opts.disabled && !opts.active) {
b.addEventListener('click', function () { goTo(opts.go); });
}
return b;
}
function renderPager() {
pager.innerHTML = '';
var tp = totalPages();
var prevBtn = makeButton('<span class="arrow" aria-hidden="true">‹</span>', { nav: true, disabled: page === 1, go: page - 1 });
prevBtn.setAttribute('aria-label', 'Previous page');
pager.appendChild(prevBtn);
for (var i = 1; i <= tp; i++) {
var nb = makeButton(String(i), { active: i === page, go: i });
nb.setAttribute('aria-label', 'Page ' + i);
pager.appendChild(nb);
}
var nextBtn = makeButton('<span class="arrow" aria-hidden="true">›</span>', { nav: true, disabled: page === tp, go: page + 1 });
nextBtn.setAttribute('aria-label', 'Next page');
pager.appendChild(nextBtn);
}
function goTo(n) {
var tp = totalPages();
if (n < 1 || n > tp || n === page) return;
page = n;
renderGrid();
renderPager();
scrollTop();
}
function scrollTop() {
var head = document.querySelector('.nw-listing');
if (head) {
var top = head.getBoundingClientRect().top + window.scrollY - 70;
window.scrollTo({ top: top, behavior: 'smooth' });
}
}
/* ---------- category tabs ---------- */
if (tabsWrap) {
var tabs = tabsWrap.querySelectorAll('.nw-tab');
tabs.forEach(function (t) {
t.addEventListener('click', function () {
if (t.classList.contains('is-active')) return;
currentCat = t.getAttribute('data-cat');
tabs.forEach(function (x) {
x.classList.remove('is-active');
x.setAttribute('aria-selected', 'false');
});
t.classList.add('is-active');
t.setAttribute('aria-selected', 'true');
page = 1;
renderGrid();
renderPager();
scrollTop();
});
});
}
/* ---------- boot ---------- */
renderGrid();
renderPager();
})();