script.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. // FeHelper Website JavaScript
  2. // DOM Ready
  3. document.addEventListener('DOMContentLoaded', function() {
  4. initNavigation();
  5. initToolTabs();
  6. initScrollAnimations();
  7. initParallaxEffects();
  8. // fetchGitHubStats(); // 已移除,无实际用途
  9. initSmoothScrolling();
  10. initPreviewImageModal();
  11. });
  12. // Navigation functionality
  13. function initNavigation() {
  14. const navbar = document.querySelector('.navbar');
  15. const navToggle = document.querySelector('.nav-toggle');
  16. const navMenu = document.querySelector('.nav-menu');
  17. // 部分页面(例如隐私政策页)不渲染顶部导航,要做存在性守卫,避免脚本报错
  18. if (!navbar) return;
  19. // Navbar scroll effect
  20. window.addEventListener('scroll', () => {
  21. if (window.scrollY > 50) {
  22. navbar.style.background = 'rgba(255, 255, 255, 0.98)';
  23. navbar.style.boxShadow = '0 4px 6px -1px rgb(0 0 0 / 0.1)';
  24. } else {
  25. navbar.style.background = 'rgba(255, 255, 255, 0.95)';
  26. navbar.style.boxShadow = 'none';
  27. }
  28. });
  29. // Mobile menu toggle
  30. if (navToggle && navMenu) {
  31. navToggle.addEventListener('click', () => {
  32. navMenu.classList.toggle('active');
  33. navToggle.classList.toggle('active');
  34. });
  35. // Close menu when clicking on links
  36. navMenu.querySelectorAll('a').forEach(link => {
  37. link.addEventListener('click', () => {
  38. navMenu.classList.remove('active');
  39. navToggle.classList.remove('active');
  40. });
  41. });
  42. }
  43. }
  44. // Tool tabs functionality
  45. function initToolTabs() {
  46. const tabButtons = document.querySelectorAll('.tab-btn');
  47. const toolGrids = document.querySelectorAll('.tools-grid');
  48. tabButtons.forEach(button => {
  49. button.addEventListener('click', () => {
  50. const category = button.dataset.category;
  51. // Remove active class from all buttons and grids
  52. tabButtons.forEach(btn => btn.classList.remove('active'));
  53. toolGrids.forEach(grid => grid.classList.remove('active'));
  54. // Add active class to clicked button and corresponding grid
  55. button.classList.add('active');
  56. const targetGrid = document.querySelector(`[data-category="${category}"].tools-grid`);
  57. if (targetGrid) {
  58. targetGrid.classList.add('active');
  59. }
  60. // Animate the transition
  61. animateGridSwitch(targetGrid);
  62. });
  63. });
  64. }
  65. // Animate grid switch
  66. function animateGridSwitch(grid) {
  67. if (!grid) return;
  68. const cards = grid.querySelectorAll('.tool-card');
  69. cards.forEach((card, index) => {
  70. card.style.opacity = '0';
  71. card.style.transform = 'translateY(20px)';
  72. setTimeout(() => {
  73. card.style.transition = 'all 0.5s ease';
  74. card.style.opacity = '1';
  75. card.style.transform = 'translateY(0)';
  76. }, index * 100);
  77. });
  78. }
  79. // Scroll animations
  80. function initScrollAnimations() {
  81. const observerOptions = {
  82. threshold: 0.1,
  83. rootMargin: '0px 0px -50px 0px'
  84. };
  85. const observer = new IntersectionObserver((entries) => {
  86. entries.forEach(entry => {
  87. if (entry.isIntersecting) {
  88. entry.target.style.opacity = '1';
  89. entry.target.style.transform = 'translateY(0)';
  90. // Special animations for different elements
  91. if (entry.target.classList.contains('feature-card')) {
  92. entry.target.style.animationDelay = '0.2s';
  93. entry.target.classList.add('animate-fade-in-up');
  94. } else if (entry.target.classList.contains('tool-card')) {
  95. entry.target.classList.add('animate-scale-in');
  96. } else if (entry.target.classList.contains('stat-item')) {
  97. animateCounter(entry.target);
  98. }
  99. }
  100. });
  101. }, observerOptions);
  102. // Observe elements for animation
  103. const animatedElements = document.querySelectorAll('.feature-card, .tool-card, .browser-card, .stat-item');
  104. animatedElements.forEach(el => {
  105. el.style.opacity = '0';
  106. el.style.transform = 'translateY(20px)';
  107. el.style.transition = 'all 0.6s ease';
  108. observer.observe(el);
  109. });
  110. }
  111. // Counter animation
  112. function animateCounter(element) {
  113. const numberElement = element.querySelector('.stat-number');
  114. if (!numberElement) return;
  115. const finalNumber = numberElement.textContent;
  116. const numericValue = parseInt(finalNumber.replace(/[^\d]/g, ''));
  117. const suffix = finalNumber.replace(/[\d.]/g, '');
  118. let currentNumber = 0;
  119. const increment = numericValue / 50;
  120. const timer = setInterval(() => {
  121. currentNumber += increment;
  122. if (currentNumber >= numericValue) {
  123. numberElement.textContent = finalNumber;
  124. clearInterval(timer);
  125. } else {
  126. numberElement.textContent = Math.floor(currentNumber) + suffix;
  127. }
  128. }, 50);
  129. }
  130. // Parallax effects
  131. function initParallaxEffects() {
  132. window.addEventListener('scroll', () => {
  133. const scrolled = window.pageYOffset;
  134. const heroPattern = document.querySelector('.hero-pattern');
  135. const browserMockup = document.querySelector('.browser-mockup');
  136. if (heroPattern) {
  137. heroPattern.style.transform = `translateY(${scrolled * 0.5}px)`;
  138. }
  139. if (browserMockup && scrolled < window.innerHeight) {
  140. browserMockup.style.transform = `perspective(1000px) rotateY(-5deg) rotateX(5deg) translateY(${scrolled * 0.1}px)`;
  141. }
  142. });
  143. }
  144. // Smooth scrolling
  145. function initSmoothScrolling() {
  146. document.querySelectorAll('a[href^="#"]').forEach(anchor => {
  147. anchor.addEventListener('click', function (e) {
  148. e.preventDefault();
  149. const target = document.querySelector(this.getAttribute('href'));
  150. if (target) {
  151. const headerOffset = 70;
  152. const elementPosition = target.getBoundingClientRect().top;
  153. const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
  154. window.scrollTo({
  155. top: offsetPosition,
  156. behavior: 'smooth'
  157. });
  158. }
  159. });
  160. });
  161. }
  162. // Add some interactive effects
  163. function addInteractiveEffects() {
  164. // Tool card hover effects
  165. document.querySelectorAll('.tool-card').forEach(card => {
  166. card.addEventListener('mouseenter', function() {
  167. this.style.transform = 'translateY(-8px) scale(1.02)';
  168. });
  169. card.addEventListener('mouseleave', function() {
  170. this.style.transform = 'translateY(0) scale(1)';
  171. });
  172. });
  173. // Browser mockup interaction
  174. const browserMockup = document.querySelector('.browser-mockup');
  175. if (browserMockup) {
  176. let isAnimating = false;
  177. browserMockup.addEventListener('mouseenter', function() {
  178. if (!isAnimating) {
  179. isAnimating = true;
  180. this.style.transform = 'perspective(1000px) rotateY(0deg) rotateX(0deg) scale(1.05)';
  181. setTimeout(() => {
  182. isAnimating = false;
  183. }, 300);
  184. }
  185. });
  186. browserMockup.addEventListener('mouseleave', function() {
  187. this.style.transform = 'perspective(1000px) rotateY(-5deg) rotateX(5deg) scale(1)';
  188. });
  189. }
  190. // Download button effects
  191. document.querySelectorAll('.download-btn').forEach(btn => {
  192. btn.addEventListener('mouseenter', function() {
  193. const arrow = this.querySelector('.btn-arrow');
  194. if (arrow) {
  195. arrow.style.transform = 'translateX(8px)';
  196. }
  197. });
  198. btn.addEventListener('mouseleave', function() {
  199. const arrow = this.querySelector('.btn-arrow');
  200. if (arrow) {
  201. arrow.style.transform = 'translateX(0)';
  202. }
  203. });
  204. });
  205. }
  206. // Initialize interactive effects after DOM is loaded
  207. document.addEventListener('DOMContentLoaded', addInteractiveEffects);
  208. // Performance optimization: Throttle scroll events
  209. function throttle(func, limit) {
  210. let inThrottle;
  211. return function() {
  212. const args = arguments;
  213. const context = this;
  214. if (!inThrottle) {
  215. func.apply(context, args);
  216. inThrottle = true;
  217. setTimeout(() => inThrottle = false, limit);
  218. }
  219. }
  220. }
  221. // Apply throttling to scroll events
  222. window.addEventListener('scroll', throttle(function() {
  223. // Scroll-based animations here
  224. }, 16)); // ~60fps
  225. // Add CSS animations dynamically
  226. const style = document.createElement('style');
  227. style.textContent = `
  228. @keyframes animate-fade-in-up {
  229. from {
  230. opacity: 0;
  231. transform: translateY(30px);
  232. }
  233. to {
  234. opacity: 1;
  235. transform: translateY(0);
  236. }
  237. }
  238. @keyframes animate-scale-in {
  239. from {
  240. opacity: 0;
  241. transform: scale(0.9);
  242. }
  243. to {
  244. opacity: 1;
  245. transform: scale(1);
  246. }
  247. }
  248. .animate-fade-in-up {
  249. animation: animate-fade-in-up 0.6s ease forwards;
  250. }
  251. .animate-scale-in {
  252. animation: animate-scale-in 0.6s ease forwards;
  253. }
  254. /* Mobile menu styles */
  255. @media (max-width: 768px) {
  256. .nav-menu {
  257. position: fixed;
  258. top: 70px;
  259. left: 0;
  260. right: 0;
  261. background: white;
  262. flex-direction: column;
  263. padding: 20px;
  264. box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  265. transform: translateY(-100%);
  266. opacity: 0;
  267. visibility: hidden;
  268. transition: all 0.3s ease;
  269. }
  270. .nav-menu.active {
  271. transform: translateY(0);
  272. opacity: 1;
  273. visibility: visible;
  274. }
  275. .nav-toggle.active span:nth-child(1) {
  276. transform: rotate(45deg) translate(5px, 5px);
  277. }
  278. .nav-toggle.active span:nth-child(2) {
  279. opacity: 0;
  280. }
  281. .nav-toggle.active span:nth-child(3) {
  282. transform: rotate(-45deg) translate(7px, -6px);
  283. }
  284. }
  285. `;
  286. document.head.appendChild(style);
  287. // Add loading states for GitHub data
  288. function showLoadingState() {
  289. const starsElement = document.getElementById('github-stars');
  290. const forksElement = document.getElementById('github-forks');
  291. if (starsElement) {
  292. starsElement.innerHTML = '<span class="loading"></span>';
  293. }
  294. if (forksElement) {
  295. forksElement.innerHTML = '<span class="loading"></span>';
  296. }
  297. }
  298. // Enhanced error handling
  299. window.addEventListener('error', function(e) {
  300. console.log('Error caught:', e.error);
  301. // Graceful degradation - keep functionality working
  302. });
  303. // Service Worker registration for PWA features (optional)
  304. if ('serviceWorker' in navigator) {
  305. window.addEventListener('load', function() {
  306. // Uncomment if you want to add PWA features
  307. // navigator.serviceWorker.register('/sw.js');
  308. });
  309. }
  310. // Export functions for testing (if needed)
  311. if (typeof module !== 'undefined' && module.exports) {
  312. module.exports = {
  313. formatNumber,
  314. throttle
  315. };
  316. }
  317. // Fetch all platform users via shields.io
  318. async function fetchAllPlatformUsers() {
  319. // Shields.io JSON API
  320. const sources = [
  321. {
  322. name: 'Chrome',
  323. url: 'https://img.shields.io/chrome-web-store/users/pkgccpejnmalmdinmhkkfafefagiiiad.json',
  324. },
  325. {
  326. name: 'Edge',
  327. url: 'https://img.shields.io/microsoftedge/addons/users/feolnkbgcbjmamimpfcnklggdcbgakhe.json',
  328. }
  329. ];
  330. let total = 0;
  331. let details = [];
  332. for (const src of sources) {
  333. try {
  334. const res = await fetch(src.url);
  335. const data = await res.json();
  336. // data.value 例:"200k"
  337. let value = data.value.replace(/[^0-9kK+]/g, '');
  338. let num = 0;
  339. if (value.endsWith('k') || value.endsWith('K')) {
  340. num = parseFloat(value) * 1000;
  341. } else {
  342. num = parseInt(value, 10);
  343. }
  344. total += num;
  345. details.push(`${src.name}: ${data.value}`);
  346. } catch (e) {
  347. details.push(`${src.name}: --`);
  348. }
  349. }
  350. // 格式化总数
  351. let totalStr = total >= 1000 ? (total / 1000).toFixed(1) + 'K+' : total + '+';
  352. const numEl = document.getElementById('all-users-number');
  353. if(numEl) numEl.textContent = totalStr;
  354. const statEl = document.getElementById('all-users-stat');
  355. if(statEl) statEl.title = details.join(',');
  356. }
  357. // 工具界面预览区图片放大查看
  358. function initPreviewImageModal() {
  359. const imgs = document.querySelectorAll('.tool-preview .preview-item img');
  360. imgs.forEach(img => {
  361. img.addEventListener('click', function() {
  362. // 创建弹窗元素
  363. const modal = document.createElement('div');
  364. modal.className = 'img-modal';
  365. modal.innerHTML = `
  366. <span class="img-modal-close" title="关闭">&times;</span>
  367. <img src="${img.src}" alt="${img.alt}" />
  368. `;
  369. document.body.appendChild(modal);
  370. // 关闭事件
  371. modal.querySelector('.img-modal-close').onclick = () => document.body.removeChild(modal);
  372. modal.onclick = (e) => {
  373. if (e.target === modal) document.body.removeChild(modal);
  374. };
  375. });
  376. });
  377. }