script.js 15 KB

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