1/* ============================================================ 2 ç±³é²è°·å¿çæ²»çæ â ä¸»è¦ JS 3 ============================================================ */ 4 5(function () { 6 'use strict'; 7 8 /* ââ 1. Header scroll è¡çº ââ */ 9 const header = document.getElementById('site-header'); 10 let lastScrollY = window.scrollY; 11 let ticking = false; 12 13 const anchorNav = document.querySelector('.anchor-nav'); 14 15 function handleScroll() { 16 const currentScrollY = window.scrollY; 17 if (currentScrollY <= 0) { 18 header.classList.remove('hide'); 19 if (anchorNav) anchorNav.classList.remove('header-hidden'); 20 } else if (currentScrollY > lastScrollY && currentScrollY > 80) { 21 header.classList.add('hide'); 22 if (anchorNav) anchorNav.classList.add('header-hidden'); 23 } else if (currentScrollY < lastScrollY) { 24 header.classList.remove('hide'); 25 if (anchorNav) anchorNav.classList.remove('header-hidden'); 26 } 27 lastScrollY = currentScrollY; 28 ticking = false; 29 } 30 31 window.addEventListener('scroll', function () { 32 if (!ticking) { 33 window.requestAnimationFrame(handleScroll); 34 ticking = true; 35 } 36 }, { passive: true }); 37 38 39 /* ââ 2. Hamburger menu ââ */ 40 const hamburger = document.getElementById('hamburger'); 41 const mainNav = document.getElementById('main-nav'); 42 43 if (hamburger && mainNav) { 44 hamburger.addEventListener('click', function () { 45 const isOpen = mainNav.classList.toggle('open'); 46 hamburger.classList.toggle('open', isOpen); 47 hamburger.setAttribute('aria-expanded', isOpen); 48 }); 49 50 // 黿 nav é£çµå¾èªåæ¶å 51 mainNav.querySelectorAll('a').forEach(function (link) { 52 link.addEventListener('click', function () { 53 mainNav.classList.remove('open'); 54 hamburger.classList.remove('open'); 55 hamburger.setAttribute('aria-expanded', 'false'); 56 }); 57 }); 58 59 // 黿å¤é¨éé 60 document.addEventListener('click', function (e) { 61 if (!header.contains(e.target)) { 62 mainNav.classList.remove('open'); 63 hamburger.classList.remove('open'); 64 hamburger.setAttribute('aria-expanded', 'false'); 65 } 66 }); 67 } 68 69 70 /* ââ 3. LINE Modal ââ */ 71 const lineBtn = document.getElementById('lineBtn'); 72 const lineModal = document.getElementById('lineModal'); 73 const modalClose = document.getElementById('modalClose'); 74 75 function openModal() { 76 if (lineModal) lineModal.classList.add('open'); 77 document.body.style.overflow = 'hidden'; 78 } 79 80 function closeModal() { 81 if (lineModal) lineModal.classList.remove('open'); 82 document.body.style.overflow = ''; 83 } 84 85 if (lineBtn) lineBtn.addEventListener('click', openModal); 86 if (modalClose) modalClose.addEventListener('click', closeModal); 87 88 if (lineModal) { 89 lineModal.addEventListener('click', function (e) { 90 if (e.target === lineModal) closeModal(); 91 }); 92 } 93 94 document.addEventListener('keydown', function (e) { 95 if (e.key === 'Escape') closeModal(); 96 }); 97 98 99 /* ââ 4. Active nav link ââ */ 100 const currentPath = window.location.pathname.split('/').pop() || 'index.html'; 101 document.querySelectorAll('.main-nav a').forEach(function (link) { 102 const linkPath = link.getAttribute('href').split('/').pop(); 103 if (linkPath === currentPath || (currentPath === '' && linkPath === 'index.html')) { 104 link.classList.add('active'); 105 } 106 }); 107 108 109 /* ââ 5. Tab åæï¼é¦é å¤é¤¨ï¼ ââ */ 110 const tabBtns = document.querySelectorAll('.tab-btn'); 111 const tabPanels = document.querySelectorAll('.tab-panel'); 112 113 tabBtns.forEach(function (btn) { 114 btn.addEventListener('click', function () { 115 const target = btn.dataset.tab; 116 117 tabBtns.forEach(function (b) { b.classList.remove('active'); }); 118 tabPanels.forEach(function (p) { p.classList.remove('active'); }); 119 120 btn.classList.add('active'); 121 const panel = document.getElementById('tab-' + target); 122 if (panel) panel.classList.add('active'); 123 }); 124 }); 125 126 127 /* ââ 6. ææ°æ¶æ¯æ¸²æï¼é¦é ï¼è³æä¾èª news.jsonï¼ ââ */ 128 const newsContainer = document.getElementById('news-container'); 129 if (newsContainer) { 130 fetch('news.json', { cache: 'no-cache' }) 131 .then(function (res) { 132 if (!res.ok) throw new Error('news fetch failed: ' + res.status); 133 return res.json(); 134 }) 135 .then(function (newsData) { 136 const active = newsData.filter(function (item) { return item.status === 'active'; }); 137 const sorted = active.sort(function (a, b) { 138 if (a.pinned !== b.pinned) return a.pinned ? -1 : 1; 139 if ((a.order || 0) !== (b.order || 0)) return (a.order || 0) - (b.order || 0); 140 return new Date(b.date) - new Date(a.date); 141 }); 142 const latest = sorted.slice(0, 6); // é¦é æå¤é¡¯ç¤ºå¹¾ç¯ï¼å¯èª¿æ´ 143 function escapeHtml(s) { 144 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 145 return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]; 146 }); 147 } 148 function safeHref(h) { 149 if (h == null) return null; 150 const s = String(h).trim(); 151 if (s === '') return null; 152 const scheme = s.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/); 153 if (scheme && !/^(https?|mailto|tel)$/i.test(scheme[1])) return null; // æ javascript:/data: ç 154 return s; 155 } 156 newsContainer.innerHTML = latest.map(function (item) { 157 const href = safeHref(item.linkHref); 158 const link = href 159 ? `<a href="${escapeHtml(href)}" class="news-link">${escapeHtml(item.linkText)} â</a>` 160 : ''; 161 return ` 162 <article class="news-card${item.pinned ? ' is-pinned' : ''}">
163 <span class="news-category">${escapeHtml(item.category)}</span> 164 <h3 class="news-title">${escapeHtml(item.title)}</h3> 165 <p class="news-date">${escapeHtml(item.dateDisplay)}</p> 166 <p class="news-summary">${escapeHtml(item.summary)}</p> 167 ${link} 168 </article> 169 `; 170 }).join(''); 171 anchorNewsReady = true; anchorTryScroll(); 172 }) 173 .catch(function (err) { 174 console.error(err); 175 newsContainer.innerHTML = '<p class="news-empty">ææ°æ¶æ¯æ«æç¡æ³è¼å ¥ï¼è«ç¨å¾å試ã</p>'; 176 anchorNewsReady = true; anchorTryScroll(); 177 }); 178 } 179 180 181 /* ââ è·¨é é¨é»ï¼é¦é ï¼ï¼å»¶å¾å°å §å®¹è¼å ¥å¾ä¸æ¬¡æ²å°ä½ ââ */ 182 // é¦é ææ°æ¶æ¯çºé忥 fetchï¼æå¨åçè·³è½ãä¹å¾ãæ¨ç§»çé¢ï¼é æå¾ contact çé 183 // é£å° index.html#locations-heading æãå è·³é¯ãåè·³å°ããè§£æ³ï¼å ç§»é¤ hash 黿¢ 184 // åçè·³è½ï¼å¾ ææ°æ¶æ¯æ¸²æå® + åçè¼å ¥å®ï¼å䏿¬¡ instant æ²å°ç®æ¨ï¼å é¦é éè¦ï¼ 185 // 以 #news-container æ¯å¦åå¨å¤æ·ï¼yucai çé ä¸åå½±é¿ï¼ã 186 var anchorPendingId = null; 187 var anchorNewsReady = false; 188 var anchorLoadReady = false; 189 function anchorTryScroll() { 190 if (!anchorPendingId || !anchorNewsReady || !anchorLoadReady) return; 191 var el = document.getElementById(anchorPendingId); 192 if (el) { 193 el.scrollIntoView({ block: 'start', behavior: 'instant' }); 194 if (window.history && history.replaceState) { 195 history.replaceState(null, '', window.location.pathname + window.location.search + '#' + anchorPendingId); 196 } 197 } 198 anchorPendingId = null; 199 } 200 if (document.getElementById('news-container') && window.location.hash.length > 1) { 201 try { anchorPendingId = decodeURIComponent(window.location.hash.slice(1)); } catch (e) { anchorPendingId = null; } 202 if (anchorPendingId && window.history && history.replaceState) { 203 history.replaceState(null, '', window.location.pathname + window.location.search); 204 } 205 } 206 window.addEventListener('load', function () { anchorLoadReady = true; anchorTryScroll(); }); 207 208 /* ââ å館æåæé + å³æçæ ï¼index.htmlï¼ ââ */ 209 // æ ¼å¼ï¼ææå¹¾(0=æ¥ .. 6=å ): [éå§æ, çµææ]ï¼null=伿¯ï¼æ { byAppt: true } 表é ç´å¶ã 210 const LOCATION_HOURS = { 211 // ä¸åï¼ä¸ä¸äº 10â21ï¼äºå 13â21ï¼å 9â17 212 'zhonghe': { 1: [10, 21], 2: [13, 21], 3: [10, 21], 4: [13, 21], 5: [10, 21], 6: [9, 17], 0: null }, 213 // æ¿æ©æäººé¨ï¼ä¸äº 13â21ï¼ä¸åäº 10â21ï¼å 9â18 214 'banqiao': { 1: [13, 21], 2: [13, 21], 3: [10, 21], 4: [10, 21], 5: [10, 21], 6: [9, 18], 0: null }, 215 // æ¿æ©å éé¨ï¼ä¸äºåäº 13â21ï¼ä¸ 10â21ï¼å 9â17 216 'banqiao-kids': { 1: [13, 21], 2: [13, 21], 3: [10, 21], 4: [13, 21], 5: [13, 21], 6: [9, 17], 0: null }, 217 // æ°åºï¼ä¸è³äº 13â21ï¼å 9â18 218 'xindian': { 1: [13, 21], 2: [13, 21], 3: [13, 21], 4: [13, 21], 5: [13, 21], 6: [9, 18], 0: null }, 219 'online': { byAppt: true }, 220 }; 221 const locationCards = document.querySelectorAll('.tab-panel[id^="tab-"] .location-card'); 222 if (locationCards.length) { 223 const LH_DOW = { 1: 'é±ä¸', 2: 'é±äº', 3: 'é±ä¸', 4: 'é±å', 5: 'é±äº', 6: 'é±å ', 0: '鱿¥' }; 224 const LH_ORDER = [1, 2, 3, 4, 5, 6, 0]; 225 const lhNow = new Date(); 226 const lhToday = lhNow.getDay(); 227 const lhHour = lhNow.getHours() + lhNow.getMinutes() / 60; 228 function lhFmt(r) { 229 return r ? (String(r[0]).padStart(2, '0') + ':00 â ' + String(r[1]).padStart(2, '0') + ':00') : '伿¯'; 230 } 231 locationCards.forEach(function (card) { 232 const panel = card.closest('.tab-panel'); 233 const data = panel ? LOCATION_HOURS[panel.id.replace(/^tab-/, '')] : null; 234 if (!data) return; 235 const name = card.querySelector('.location-name'); 236 if (name && !name.querySelector('.loc-status')) { 237 let badge; 238 if (data.byAppt) { 239 badge = '<span class="loc-status appt"><span class="dot"></span>é ç´å¶</span>'; 240 } else { 241 const r = data[lhToday]; 242 const open = !!(r && lhHour >= r[0] && lhHour < r[1]); 243 badge = open 244 ? '<span class="loc-status open"><span class="dot"></span>æåä¸</span>' 245 : '<span class="loc-status closed"><span class="dot"></span>å·²æç</span>'; 246 } 247 name.insertAdjacentHTML('beforeend', badge); 248 } 249 const dd = card.querySelector('.loc-hours-dd'); 250 if (dd) { 251 if (data.byAppt) { 252 dd.innerHTML = '<div class="loc-hours"><div class="loc-hours-row"><span class="d">é ç´å¶</span>' + 253 '<span class="t" style="color:var(--color-text-muted)">ä¾åå¿çå¸«ææ®µï¼è«ä¾é»æç·ä¸é ç´</span></div></div>'; 254 } else { 255 dd.innerHTML = '<div class="loc-hours">' + LH_ORDER.map(function (d) { 256 const r = data[d]; 257 const isToday = d === lhToday; 258 const tag = isToday ? '<span class="loc-hours-today-tag">ä»å¤©</span>' : ''; 259 const t = r ? '<span class="t">' + lhFmt(r) + '</span>
259' : '<span class="t closed">伿¯</span>'; 260 return '<div class="loc-hours-row' + (isToday ? ' is-today' : '') + '"><span class="d">' + LH_DOW[d] + '</span>' + t + tag + '</div>'; 261 }).join('') + '</div>'; 262 } 263 } 264 }); 265 } 266 267 /* ââ å館空éç §çç¸ç°¿ï¼index.htmlã»ç¸®å + lightboxï¼ ââ */ 268 // å¡«å ¥çå¯¦ç §çå¾ï¼è©²é¤¨å¡ç峿åºç¾ã空éç°å¢ãç¸ç°¿ï¼ç©ºé£åï¼ä¸é¡¯ç¤ºã 269 // æ ¼å¼ï¼æ¯é¤¨ä¸åé£åï¼æ¯å¼µ { num: ç·¨èæ null, label: 顯示å稱, src: 'ç §çè·¯å¾' }ã 270 // ç §çæ¾ assets/images/spaces/ï¼å½åå¦ zhonghe-1.jpgãç¯ä¾ï¼ 271 // 'zhonghe': [ 272 // { num: 1, label: 'æ²»ç室 1', src: 'assets/images/spaces/zhonghe/1.jpg' }, 273 // { num: null, label: 'å診å', src: 'assets/images/spaces/zhonghe/5.jpg' }, 274 // ], 275 // â ï¸ æ«å®ç §çï¼2026-07ï¼ï¼åé¤¨å æ¾æ¢æç©ºéç §å ä½ï¼label çµ±ä¸ã空éç°å¢ãï¼ 276 // å¾ çå¯¦ç §çèæ£å¼å稱å°ä½å¾ï¼ç´æ¥æ¿æ srcï¼label å³å¯ï¼ç©ºé£åï¼ä¸é¡¯ç¤ºï¼ã 277 const LOCATION_SPACES = { 278 'zhonghe': [ 279 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/1.jpg' }, 280 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/2.jpg' }, 281 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/3.jpg' }, 282 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/4.jpg' }, 283 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/5.jpg' }, 284 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/6.jpg' }, 285 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/7.jpg' }, 286 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/8.jpg' }, 287 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/9.jpg' }, 288 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/zhonghe/10.jpg' }, 289 ], 290 'banqiao': [ 291 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao/1.jpg' }, 292 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao/2.webp' }, 293 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao/3.webp' }, 294 ], 295 'banqiao-kids': [ 296 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/1.jpg' }, 297 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/2.jpg' }, 298 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/3.jpg' }, 299 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/4.jpg' }, 300 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/5.jpg' }, 301 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/6.jpg' }, 302 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/7.jpg' }, 303 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/8.jpg' }, 304 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/9.jpg' }, 305 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/10.jpg' }, 306 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/banqiao-kids/11.jpg' }, 307 ], 308 'xindian': [ 309 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/xindian/1.jpg' }, 310 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/xindian/2.jpg' }, 311 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/xindian/3.jpg' }, 312 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/xindian/4.jpg' }, 313 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/xindian/5.jpg' }, 314 { num: null, label: '空éç°å¢', src: 'assets/images/spaces/xindian/6.jpg' }, 315 ], 316 }; 317 const spaceCards = document.querySelectorAll('.tab-panel[id^="tab-"] .location-card'); 318 if (spaceCards.length) { 319 let lbList = []; 320 let lbIndex = 0; 321 let lbEl = null; 322 function showLb(i) { 323 if (!lbList.length) return; 324 lbIndex = (i + lbList.length) % lbList.length; 325 const p = lbList[lbIndex]; 326 const img = lbEl.querySelector('.space-lb-stage img'); 327 img.src = p.src; img.alt = p.label; 328 lbEl.querySelector('.space-lb-cap').innerHTML = (p.num ? '<span class="space-num">' + p.num + '</span>' : '') + p.label; 329 } 330 function closeLb() { 331 if (!lbEl) return; 332 lbEl.classList.remove('open'); 333 document.body.classList.remove('space-lb-open'); 334 } 335 function buildLb() { 336 lbEl = document.createElement('div'); 337 lbEl.className = 'space-lightbox'; 338 lbEl.setAttribute('role', 'dialog'); 339 lbEl.setAttribute('aria-modal', 'true'); 340 lbEl.innerHTML = 341 '<button type="button" class="space-lb-close" aria-label="éé">Ã</button>' + 342 '<button type="button" class="space-lb-nav space-lb-prev" aria-label="ä¸ä¸å¼µ">â¹</button>' + 343 '<figure class="space-lb-stage"><img alt="" /><figcaption class="space-lb-cap"></figcaption></figure>' + 344 '<button type="button" class="space-lb-nav space-lb-next" aria-label="ä¸ä¸å¼µ">âº</button>'; 345 document.body.appendChild(lbEl); 346 lbEl.querySelector('.space-lb-close').addEventListener('click', closeLb); 347 lbEl.querySelector('.space-lb-prev').addEventListener('click', function () { showLb(lbIndex - 1); }); 348 lbEl.querySelector('.space-lb-next').addEventListener('click', function () { showLb(lbIndex + 1); }); 349 lbEl.addEventListener('click', function (e) { if (e.target === lbEl) closeLb(); }); 350 document.addEventListener('keydown', function (e) { 351 if (!lbEl || !lbEl.classList.contains('open')) return; 352 if (e.key === 'Escape') closeLb(); 353 else if (e.key === 'ArrowLeft') showLb(lbIndex - 1); 354 else if (e.key === 'ArrowRight') showLb(lbIndex + 1); 355 }); 356 } 357 function openLb(list, i) { 358 if (!lbEl) buildLb(); 359 lbList = list; 360 lbEl.classList.add('open'); 361 document.body.classList.add('space-lb-open'); 362 showLb(i); 363 } 364 spaceCards.forEach(function (card) { 365 const panel = card.closest('.tab-panel');
366 const photos = panel ? LOCATION_SPACES[panel.id.replace(/^tab-/, '')] : null; 367 if (!photos || !photos.length) return; 368 const gallery = document.createElement('div'); 369 gallery.className = 'location-spaces'; 370 gallery.innerHTML = '<p class="location-spaces-title">空éç°å¢</p>' + 371 '<div class="location-spaces-viewport">' + 372 '<button type="button" class="space-arrow space-arrow-prev is-hidden" aria-label="ä¸ä¸æ¹ç §ç">â¹</button>' + 373 '<div class="location-spaces-grid">' + photos.map(function (p, i) { 374 return '<button type="button" class="space-thumb" data-i="' + i + '">' + 375 '<img src="' + p.src + '" alt="' + p.label + '" loading="lazy" />' + 376 '<span class="space-cap">' + (p.num ? '<span class="space-num">' + p.num + '</span>' : '') + p.label + '</span>' + 377 '</button>'; 378 }).join('') + '</div>' + 379 '<button type="button" class="space-arrow space-arrow-next" aria-label="ä¸ä¸æ¹ç §ç">âº</button>' + 380 '</div>'; 381 card.appendChild(gallery); 382 const grid = gallery.querySelector('.location-spaces-grid'); 383 const arrowPrev = gallery.querySelector('.space-arrow-prev'); 384 const arrowNext = gallery.querySelector('.space-arrow-next'); 385 function updateArrows() { 386 const max = grid.scrollWidth - grid.clientWidth; 387 arrowPrev.classList.toggle('is-hidden', grid.scrollLeft <= 1); 388 arrowNext.classList.toggle('is-hidden', grid.scrollLeft >= max - 1); 389 } 390 arrowPrev.addEventListener('click', function () { grid.scrollBy({ left: -grid.clientWidth * 0.8, behavior: 'smooth' }); }); 391 arrowNext.addEventListener('click', function () { grid.scrollBy({ left: grid.clientWidth * 0.8, behavior: 'smooth' }); }); 392 grid.addEventListener('scroll', updateArrows, { passive: true }); 393 window.addEventListener('resize', updateArrows); 394 updateArrows(); 395 gallery.querySelectorAll('.space-thumb').forEach(function (btn) { 396 btn.addEventListener('click', function () { openLb(photos, parseInt(btn.dataset.i, 10)); }); 397 }); 398 }); 399 } 400 401 /* ââ 7. åé篩é¸ï¼team.htmlï¼ ââ */ 402 const teamGrid = document.getElementById('team-grid'); 403 if (teamGrid && typeof teamData !== 'undefined') { 404 // å¿ç師æåºï¼æé· / å¯æé· â 主任 â å ¶ä»ï¼å層ç´ä¾è³æåæ¬é åºï¼ 405 function roleRank(role) { 406 if (!role) return 2; 407 if (role.indexOf('æé·') !== -1) return 0; 408 if (role.indexOf('主任') !== -1) return 1; 409 return 2; 410 } 411 412 // 渲æå¿ç師 413 const therapists = teamData 414 .filter(function (m) { return m.type === 'therapist'; }) 415 .map(function (m, i) { return { m: m, i: i }; }) 416 .sort(function (a, b) { 417 const r = roleRank(a.m.role) - roleRank(b.m.role); 418 return r !== 0 ? r : a.i - b.i; 419 }) 420 .map(function (x) { return x.m; }); 421 const socialWorkers = teamData.filter(function (m) { return m.type === 'social-worker'; }); 422 const admins = teamData.filter(function (m) { return m.type === 'admin'; }); 423 424 therapists.forEach(function (member) { 425 const card = createTeamCard(member); 426 teamGrid.appendChild(card); 427 }); 428 429 // 渲æç¤¾å·¥å¸« 430 const socialWorkerGrid = document.getElementById('social-worker-grid'); 431 if (socialWorkerGrid) { 432 socialWorkers.forEach(function (member) { 433 const card = createTeamCard(member); 434 socialWorkerGrid.appendChild(card); 435 }); 436 } 437 438 // 渲æè¡æ¿ 439 const adminGrid = document.getElementById('admin-grid'); 440 if (adminGrid) { 441 admins.forEach(function (member) { 442 const card = createTeamCard(member); 443 adminGrid.appendChild(card); 444 }); 445 } 446 447 // 篩é¸é輯 448 // 館å¥çºå¤é¸ï¼OR é輯ï¼ï¼activeLocations çºç©ºé£å代表ãå ¨é¨ã 449 let activeLocations = []; 450 let activeTarget = 'all'; 451 let activeScheme = 'all'; 452 // å°é·çºå¤é¸ï¼OR é輯ï¼ï¼activeSpecialties çºç©ºé£å代表ãå ¨é¨ã 453 let activeSpecialties = []; 454 455 // å¸¸è¦æåé ç®ï¼èæåé ï¼åå·ä¸è´ç 12 é ï¼ 456 const COMMON_SPECIALTIES = [ 457 '人éè°é¡', 'å£å調é©', 'ç¡ç å°æ¾', 'çæ´»é©æ', 458 'èªææ¢ç´¢', 'æ ç·èª¿é©', '親è·åå©', 'éå°å¹´é©æ', 459 'å©å§»/ä¼´ä¾¶è°é¡', 'çæ¶¯è¦å', 'å ç«¥æ ç·/è¡çº', 'å 童人éäºå', 460 ]; 461 462 // ç± teamData æ¶éææãåäººç¹æ®å°é·ãï¼specialties + generalSpecialties ä¸ä¸å±¬æ¼ 12 é 常è¦é ç®çå¼é½ç´å ¥ 463 const specialSet = new Set();
464 therapists.forEach(function (m) { 465 [].concat(m.specialties || [], m.generalSpecialties || []).forEach(function (s) { 466 if (!COMMON_SPECIALTIES.includes(s)) specialSet.add(s); 467 }); 468 }); 469 const SPECIAL_SPECIALTIES = Array.from(specialSet).sort(function (a, b) { 470 return a.localeCompare(b, 'zh-Hant'); 471 }); 472 473 // çºæ¯å¼µå¡çå¯«å ¥åä½µå¾ç specialties data-attrï¼generalSpecialties + specialtiesï¼ 474 teamGrid.querySelectorAll('.team-card').forEach(function (card, i) { 475 const m = therapists[i]; 476 if (!m) return; 477 const combined = [].concat(m.generalSpecialties || [], m.specialties || []); 478 card.dataset.specialties = combined.join('|'); 479 }); 480 481 // 渲æ modal å §å®¹ 482 const specialtyTrigger = document.getElementById('specialtyTrigger'); 483 const specialtyModal = document.getElementById('specialtyModal'); 484 if (specialtyTrigger && specialtyModal) { 485 const modalBody = document.getElementById('specialtyModalBody'); 486 const searchInput = document.getElementById('specialtySearch'); 487 const applyBtn = document.getElementById('specialtyApply'); 488 const cancelBtn = document.getElementById('specialtyCancel'); 489 const clearBtn = document.getElementById('specialtyClear'); 490 const closeBtn = document.getElementById('specialtyModalClose'); 491 const chipsEl = document.getElementById('specialtyChips'); 492 493 function renderGroup(title, items) { 494 if (!items.length) return ''; 495 const checks = items.map(function (v) { 496 const safe = v.replace(/"/g, '"'); 497 return '<label class="specialty-check"><input type="checkbox" value="' + safe + '">' + v + '</label>'; 498 }).join(''); 499 return '<div class="specialty-group"><p class="specialty-group-title">' + title + '</p>' + 500 '<div class="specialty-check-grid">' + checks + '</div></div>'; 501 } 502 503 modalBody.innerHTML = 504 renderGroup('å¸¸è¦æåé ç®', COMMON_SPECIALTIES) + 505 renderGroup('åäººç¹æ®å°é·', SPECIAL_SPECIALTIES); 506 507 const checkboxes = modalBody.querySelectorAll('input[type="checkbox"]'); 508 509 function updateApplyLabel() { 510 const n = Array.from(checkboxes).filter(function (c) { return c.checked; }).length; 511 applyBtn.textContent = n > 0 ? 'å¥ç¨ (' + n + ')' : 'å¥ç¨'; 512 } 513 checkboxes.forEach(function (c) { c.addEventListener('change', updateApplyLabel); }); 514 515 function openModal() { 516 // é²å ¥æè® modal å §å¾é¸çæ åæ ç®åå·²å¥ç¨çæ¢ä»¶ 517 checkboxes.forEach(function (c) { c.checked = activeSpecialties.indexOf(c.value) !== -1; }); 518 searchInput.value = ''; 519 modalBody.querySelectorAll('.specialty-check').forEach(function (lbl) { lbl.style.display = ''; }); 520 modalBody.querySelectorAll('.specialty-group').forEach(function (g) { g.style.display = ''; }); 521 specialtyModal.classList.add('open'); 522 updateApplyLabel(); 523 setTimeout(function () { searchInput.focus(); }, 50); 524 } 525 function closeModal() { specialtyModal.classList.remove('open'); } 526 527 specialtyTrigger.addEventListener('click', openModal); 528 closeBtn.addEventListener('click', closeModal); 529 cancelBtn.addEventListener('click', closeModal); 530 specialtyModal.addEventListener('click', function (e) { 531 if (e.target === specialtyModal) closeModal(); 532 }); 533 document.addEventListener('keydown', function (e) { 534 if (e.key === 'Escape' && specialtyModal.classList.contains('open')) closeModal(); 535 }); 536 537 clearBtn.addEventListener('click', function () { 538 checkboxes.forEach(function (c) { c.checked = false; }); 539 updateApplyLabel(); 540 }); 541 542 searchInput.addEventListener('input', function () { 543 const q = searchInput.value.trim(); 544 modalBody.querySelectorAll('.specialty-group').forEach(function (group) { 545 let anyVisible = false; 546 group.querySelectorAll('.specialty-check').forEach(function (lbl) { 547 const visible = !q || lbl.textContent.indexOf(q) !== -1; 548 lbl.style.display = visible ? '' : 'none'; 549 if (visible) anyVisible = true; 550 }); 551 group.style.display = anyVisible ? '' : 'none'; 552 }); 553 }); 554 555 applyBtn.addEventListener('click', function () { 556 activeSpecialties = Array.from(checkboxes).filter(function (c) { return c.checked; }).map(function (c) { return c.value; }); 557 renderSpecialtyChips(); 558 applyFilter(); 559 closeModal(); 560 }); 561
562 function renderSpecialtyChips() { 563 chipsEl.innerHTML = ''; 564 activeSpecialties.forEach(function (v) { 565 const chip = document.createElement('span'); 566 chip.className = 'specialty-chip'; 567 chip.innerHTML = v.replace(/[<>&]/g, function (ch) { 568 return ({ '<': '<', '>': '>', '&': '&' })[ch]; 569 }) + ' <button type="button" aria-label="ç§»é¤ ' + v + '">Ã</button>'; 570 chip.querySelector('button').addEventListener('click', function () { 571 activeSpecialties = activeSpecialties.filter(function (s) { return s !== v; }); 572 renderSpecialtyChips(); 573 applyFilter(); 574 }); 575 chipsEl.appendChild(chip); 576 }); 577 } 578 } 579 580 document.querySelectorAll('[data-filter-location]').forEach(function (btn) { 581 btn.addEventListener('click', function () { 582 const value = btn.dataset.filterLocation; 583 if (value === 'all') { 584 activeLocations = []; 585 } else { 586 const idx = activeLocations.indexOf(value); 587 if (idx === -1) { 588 activeLocations.push(value); 589 } else { 590 activeLocations.splice(idx, 1); 591 } 592 } 593 document.querySelectorAll('[data-filter-location]').forEach(function (b) { 594 const v = b.dataset.filterLocation; 595 b.classList.toggle('active', 596 v === 'all' ? activeLocations.length === 0 : activeLocations.includes(v)); 597 }); 598 applyFilter(); 599 }); 600 }); 601 602 document.querySelectorAll('[data-filter-target]').forEach(function (btn) { 603 btn.addEventListener('click', function () {
604 activeTarget = btn.dataset.filterTarget; 605 document.querySelectorAll('[data-filter-target]').forEach(function (b) { 606 b.classList.toggle('active', b.dataset.filterTarget === activeTarget); 607 }); 608 applyFilter(); 609 }); 610 }); 611 612 document.querySelectorAll('[data-filter-scheme]').forEach(function (btn) { 613 btn.addEventListener('click', function () { 614 activeScheme = btn.dataset.filterScheme; 615 document.querySelectorAll('[data-filter-scheme]').forEach(function (b) { 616 b.classList.toggle('active', b.dataset.filterScheme === activeScheme); 617 }); 618 applyFilter(); 619 }); 620 }); 621 622 // 社工師åéçºåºå®åå®ãä¸åå¿ç師篩é¸å¨ï¼çºé¿å 使ç¨è 誤以çºç¯©é¸å°ç¤¾å·¥å¸« 623 // ä¹çæï¼ç¯©äºæ¹æ¡/館å¥/å°è±¡ï¼ç¤¾å·¥å¸«å»åå°ä¸åçå¨çé¢ä¸ï¼ï¼åªè¦ä»»ä¸ç¯©é¸ 624 // æ¢ä»¶ä¸æ¯ãå ¨é¨ãï¼å°±æ´æ®µé±è社工師åéåå¡ï¼å ¨é¨åå°ãå ¨é¨ãæé¡¯ç¤ºã 625 const socialWorkersSection = document.getElementById('social-workers-section'); 626 627 function applyFilter() { 628 teamGrid.querySelectorAll('.team-card').forEach(function (card) { 629 const locations = (card.dataset.locations || '').split(','); 630 const targets = (card.dataset.targets || '').split(','); 631 const schemes = (card.dataset.schemes || '').split(','); 632 const specialties = (card.dataset.specialties || '').split('|').filter(
632Boolean); 633 const matchLoc = activeLocations.length === 0 || activeLocations.some(function (l) { 634 return locations.includes(l); 635 }); 636 const matchTarget = activeTarget === 'all' || targets.includes(activeTarget); 637 const matchScheme = activeScheme === 'all' || schemes.includes(activeScheme); 638 const matchSpec = activeSpecialties.length === 0 || activeSpecialties.some(function (s) { 639 return specialties.includes(s); 640 }); 641 card.classList.toggle('hidden', !(matchLoc && matchTarget && matchScheme && matchSpec)); 642 }); 643 644 if (socialWorkersSection) { 645 const anyFilterActive = activeLocations.length > 0 || activeTarget !== 'all' || 646 activeScheme !== 'all' || activeSpecialties.length > 0; 647 socialWorkersSection.style.display = anyFilterActive ? 'none' : ''; 648 } 649 } 650 } 651 652 653 /* ââ 8. Team Member é ââ */ 654 const memberContainer = document.getElementById('member-detail'); 655 if (memberContainer && typeof teamData !== 'undefined') { 656 const params = new URLSearchParams(window.location.search); 657 const memberId = params.get('id'); 658 const member = teamData.find(function (m) { return m.id === memberId; }); 659 660 if (member) { 661 document.title = member.name + ' â ç±³é²è°·å¿çæ²»çæ'; 662 663 function makeList(arr) { 664 if (!arr || !arr.length) return ''; 665 return `<ul class="member-list">${arr.map(function (e) { return `<li>${e}</li>`; }).join('')}</ul>`; 666 } 667 668 function makeExperienceList(arr) { 669 if (!arr || !arr.length) return ''; 670 if (typeof arr[0] === 'string') return makeList(arr); 671 return `<ul class="member-list">${arr.map(function (e) { 672 const badge = e.current 673 ? `<span class="exp-badge exp-badge--current">ç¾è·</span>` 674 : ''; 675 return `<li>${badge}${e.text}</li>`; 676 }).join('')}</ul>`; 677 } 678 679 function makePublicationList(arr) { 680 if (!arr || !arr.length) return ''; 681 return `<ul class="member-list">${arr.map(function (p) { 682 if (typeof p === 'string') return `<li>${p}</li>`; 683 const titleText = p.type === 'book' ? `ã${p.title}ã` : p.title; 684 const metaBits = []; 685 if (p.role) metaBits.push(p.role); 686 if (p.publisher) metaBits.push(p.publisher); 687 if (p.year) metaBits.push(p.year); 688 if (!metaBits.length && p.note) metaBits.push(p.note); 689 const meta = metaBits.length ? `<span class="pub-meta">ï¼${metaBits.join('ã»')}ï¼</span>` : ''; 690 const inner = `${titleText}${meta}`; 691 return p.url 692 ? `<li><a href="${p.url}" target="_blank" rel="noopener">${inner}</a></li>` 693 : `<li>${inner}</li>`; 694 }).join('')}</ul>`; 695 } 696 697 function makeSection(title, content) { 698 if (!content) return ''; 699 return `<div class="member-section"><p class="member-section-title">${title}</p>${content}</div>`; 700 } 701 702 // ç£å°æ å¢ï¼?view=supervision ä¸è©²å¿ç師æç£å°è³ææï¼ä¸æ¹é¡¯ç¤ºãç£å°ç¸éè³æãåå¡ 703 const isSupervisionView = params.get('view') === 'supervision' && !!member.supervision; 704 function escSup(v) { 705 return String(v == null ? '' : v) 706 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 707 } 708 function supRow(label, val) { 709 return val ? `<div class="sup-row"><dt>${label}</dt><dd>${escSup(val)}</dd></div>` : ''; 710 } 711 const supervisionSection = (function () { 712 if (isSupervisionView) { 713 const s = member.supervision; 714 const st = s.style || {}; 715 const styleItems = [ 716 st.focus ? `<li><span>éæ³¨ç¦é»</span>${escSup(st.focus)}</li>` : '', 717 st.atmosphere ? `<li><span>äºåæ°å</span>${escSup(st.atmosphere)}</li>` : '', 718 st.transmission ? `<li><span>å°æ¥å³æ¿</span>${escSup(st.transmission)}</li>` : '', 719 st.gift ? `<li><span>åç£çæ¶ç©«</span>${escSup(st.gift)}</li>` : '', 720 ].join(''); 721 return ` 722 <div class="member-supervision-detail"> 723 <p class="member-section-title">ç£å°ç¸éè³æ</p> 724 ${s.tagline ? `<p class="sup-tagline">ã${escSup(s.tagline)}ã</p>` : ''} 725 <dl class="sup-list"> 726 ${supRow('ç£å°åå', s.orientation)} 727 ${supRow('å°é·æåå°è±¡', s.clientGroups)} 728 ${supRow('æ é·é å', s.challengeAreas)} 729 ${supRow('ç£å°å½¢å¼', s.formats)} 730 ${supRow('ç·ä¸ç£å°', s.online)} 731 ${supRow('實ç¿ç£å°è³æ ¼', s.internQualified)} 732 ${supRow('叿ç£å°èè', s.assocCertNo)} 733 ${supRow('ç£å°è²»ç¨', s.fee)} 734 ${supRow('å¸çåªæ ', s.studentDiscount)} 735 ${supRow('è·¨é«ç³»å¯¦åå°é·', s.crossSystem)} 736 </dl> 737 ${styleItems ? `<p class="sup-style-title">ç£å°é¢¨æ ¼</p><ul class="sup-style">${styleItems}</ul>` : ''} 738 </div>`; 739 } 740 // æ°ç¾çï¼ç¡ view=supervisionï¼ä¸å顯示ç£å°åªåååå°å¡ 741 return ''; 742 })(); 743 744 const photoEl = member.photo 745 ? (member.photoCrop
746 // å人é ï¼ç¸½è¦½å¡çä¸å¤®ç´æ¢ï¼å §å±¤ç¨ãå¡çè¦å(16:10)ãæ»¿é«ç½®ä¸ãå¤å±¤ 3:4 è£æå·¦å³ 747 ? `<div class="member-photo-strip"><img src="${member.photo}" alt="${member.name}" class="member-photo-cardview" style="${window.photoCropStyle(member.photoCrop)}" /></div>` 748 : `<img src="${member.photo}" alt="${member.name}" class="member-photo-img" />`) 749 : `<div class="member-photo-placeholder"></div>`; 750 751 const locationChips = member.locations.map(function (l) { 752 return `<span class="chip">${l}</span>`; 753 }).join(''); 754 755 const targetChips = member.targets.map(function (t) { 756 return `<span class="chip chip-primary">${t}</span>`; 757 }).join(''); 758 759 const generalSpecialtyChips = (member.generalSpecialties || []).map(function (s) { 760 return `<span class="chip chip-primary">${s}</span>`; 761 }).join(''); 762 763 const specialtyChips = member.specialties.map(function (s) { 764 return `<span class="chip">${s}</span>`; 765 }).join(''); 766 767 const linksHtml = (function () { 768 if (!member.links) return ''; 769 const parts = []; 770 if (member.links.website) parts.push(`<a href="${member.links.website}" class="member-link" target="_blank" rel="noopener">åäººç¶²ç« â</a>`); 771 if (member.links.instagram) parts.push(`<a href="https://www.instagram.com/${member.links.instagram.replace('@', '')}" class="member-link" target="_blank" rel="noopener">Instagram â</a>`); 772 if (member.links.facebook) parts.push(`<a href="${member.links.facebook}" class="member-link" target="_blank" rel="noopener">Facebook â</a>`); 773 return parts.join(''); 774 })(); 775 776 memberContainer.innerHTML = ` 777 <div class="member-layout"> 778 <div class="member-photo">${photoEl}</div> 779 <div class="member-info"> 780 <h1>${member.name}</h1> 781 <p class="member-title">${member.title}</p> 782 ${member.licenseNo ? `<p class="member-license">${member.licenseNo}</p>` : ''} 783 ${member.role ? `<p class="member-role">${member.role}</p>` : ''} 784 <div class="member-chips"> 785 ${locationChips} 786 ${targetChips} 787 </div> 788 789 ${(member.generalSpecialties && member.generalSpecialties.length) || (member.specialties && member.specialties.length) ? ` 790 <div class="member-section"> 791 <p class="member-section-title">å°é·é å</p> 792 ${member.generalSpecialties && member.generalSpecialties.length ? ` 793 <p class="member-specialty-label">å¸¸è¦æåé ç®</p><!-- èå稱ï¼ä¸è¬æ§å°é· --> 794 <div class="member-specialties" style="margin-bottom:14px">${generalSpecialtyChips}</div>` : ''} 795 ${member.specialties && member.specialties.length ? ` 796 ${member.generalSpecialties && member.generalSpecialties.length ? '<p class="member-specialty-label">ç¹æ®å°é·</p>' : ''} 797 <div class="member-specialties">${specialtyChips}</div>` : ''} 798 </div>` : ''} 799 800 ${member.schedule && member.schedule.length ? ` 801 <div class="member-section"> 802 <p class="member-section-title">åºå®ç表</p> 803 <div class="member-schedule"> 804 ${member.schedule.map(function (s) { 805 return `<span class="chip ${locationChipClass(s.location)}">${s.day}ã»${s.location}</span>`; 806 }).join('')} 807 </div> 808 <p class="member-schedule-note">⻠實éç表以é»è©±ç¢ºèªçºæº</p> 809 </div>` : ''} 810 811 ${makeSection('叿·', makeList(member.education))} 812 ${makeSection('ç¶æ·', makeExperienceList(member.experience))} 813 ${makeSection('èªèèè¨ç·´', makeList(member.certifications))} 814 ${makeSection('èä½ / åªé«', makePublicationList(member.publications))} 815 ${linksHtml ? `<div class="member-section member-links">${linksHtml}</div>` : ''} 816 ${supervisionSection} 817 </div> 818 </div> 819 `; 820 821 // ç£å°æ å¢ï¼æé å°¾ CTA æ¹çºç£å°æ´½è©¢ 822 if (isSupervisionView) { 823 const ctaBox = document.getElementById('member-cta'); 824 if (ctaBox) { 825 ctaBox.innerHTML = 826 '<h3 style="margin-bottom:12px; font-size:1.15rem;">æ³é ç´éä½ç£å°åï¼</h3>' + 827 '<p style="color:var(--color-text-muted); font-size:0.92rem; margin:0 auto 28px; max-width:560px; line-height:1.8;">填寫ç£å°ç³è«è¡¨ï¼æåæä¾æ¨çéæ±èéä½ç£å°çææ®µåå©åªåã</p>' + 828 '<div style="display:flex; gap:14px; justify-content:center; flex-wrap:wrap;">' + 829 '<a href="contact.html?view=supervision&designated=yes&name=' + encodeURIComponent(member.name) + '#supervision-heading" class="btn btn-primary">洽詢ç£å°åªå</a>' + 830 '<a href="yucai.html#sv-' + member.id + '" class="btn btn-outline">åå°ç£å°é£å®¹</a>' + 831 '</div>'; 832 } 833 } 834 } else { 835 memberContainer.innerHTML = ` 836 <div style="text-align:center; padding: 80px 0;"> 837 <p style="color:var(--color-text-muted);">æ¾ä¸å°æ¤å¿ç師çè³æã</p> 838 <a href="team.html" class="btn btn-outline" style="margin-top:24px">è¿ååéä»ç´¹</a> 839 </div> 840 `; 841 } 842 } 843 844 845 /* ââ Helper: 館å¥å°æ chip class ââ */ 846 function locationChipClass(location) { 847 var map = { 'ä¸å': 'chip-zhonghe', 'æ¿æ©': 'chip-banqiao', 'æ°åº': 'chip-xindian' }; 848 return map[location] || ''; 849 } 850 851 /* ââ Helper: å»ºç« Team Card DOM ââ */ 852 function createTeamCard(member) { 853 const isLinked = member.type === 'therapist' || member.type === 'social-worker'; 854 const card = document.createElement(isLinked ? 'a' : 'div'); 855 card.className = 'team-card'; 856 if (isLinked) card.href = `team-member.html?id=${member.id}`; 857 card.dataset.locations = member.locations.join(','); 858 card.dataset.targets = member.targets.join(','); 859 card.dataset.schemes = (member.schemes || []).join(','); 860 861 const photoEl = member.photo 862 ? `<img src="${member.photo}" alt="${member.name}" loading="lazy"${member.photoCrop ? ` style="${window.photoCropStyle(member.photoCrop)}"` : ''} />` 863 : `<div class="team-card-photo-placeholder"></div>`; 864 865 const locationTags = member.locations.map(function (l) { 866 return `<span class="chip">${l}</span>`;
867 }).join(''); 868 869 const targetTags = member.targets.map(function (t) { 870 return `<span class="chip chip-primary">${t}</span>`; 871 }).join(''); 872 873 const specialtyTags = (member.specialties || []).slice(0, 4).map(function (s) { 874 return `<span class="chip chip-muted">${s}</span>`; 875 }).join(''); 876 877 const educationHtml = (member.type === 'admin' && member.education && member.education.length) 878 ? `<div class="team-card-education">${member.education.map(function (e) { return `<p>${e}</p>`; }).join('')}</div>` 879 : ''; 880 881 card.innerHTML = ` 882 <div class="team-card-photo">${photoEl}</div> 883 <div class="team-card-body"> 884 ${member.role ? `<p class="team-card-role">${member.role}</p>` : ''} 885 <p class="team-card-name">${member.name}</p> 886 <p class="team-card-title">${member.title}</p> 887 <div class="team-card-tags"> 888 ${locationTags} 889 ${targetTags} 890 </div> 891 ${specialtyTags ? `<div class="team-card-specialties">${specialtyTags}</div>` : ''} 892 ${educationHtml} 893 </div> 894 `; 895 return card; 896 } 897 898 899 /* ââ 9. 館å¥è³è¨ / å°å åæ ââ */ 900 /* ââ 10. Map Lightbox ââ */ 901 var mapLightbox = document.getElementById('mapLightbox'); 902 var mapLightboxImg = document.getElementById('mapLightboxImg'); 903 var mapLightboxClose = document.getElementById('mapLightboxClose'); 904 905 function openMapLightbox(src, alt) { 906 mapLightboxImg.src = src; 907 mapLightboxImg.alt = alt; 908 mapLightbox.classList.add('open'); 909 document.body.style.overflow = 'hidden'; 910 } 911 912 function closeMapLightbox() { 913 mapLightbox.classList.remove('open'); 914 document.body.style.overflow = ''; 915 } 916 917 document.querySelectorAll('.location-view-panel img').forEach(function (img) { 918 img.addEventListener('click', function () { 919 openMapLightbox(img.src, img.alt); 920 }); 921 }); 922 923 if (mapLightboxClose) mapLightboxClose.addEventListener('click', closeMapLightbox); 924 if (mapLightbox) mapLightbox.addEventListener('click', function (e) { 925 if (e.target === mapLightbox) closeMapLightbox(); 926 }); 927 document.addEventListener('keydown', function (e) { 928 if (e.key === 'Escape' && mapLightbox && mapLightbox.classList.contains('open')) closeMapLightbox(); 929 }); 930 931 932 933 document.querySelectorAll('.location-view-tab').forEach(function (tab) { 934 tab.addEventListener('click', function () { 935 var right = tab.closest('.location-right'); 936 var slider = right.querySelector('.location-view-slider'); 937 var tabs = right.querySelectorAll('.location-view-tab'); 938
939 tabs.forEach(function (t) { t.classList.remove('active'); }); 940 tab.classList.add('active'); 941 942 if (tab.dataset.view === 'map') { 943 slider.classList.add('show-map'); 944 } else { 945 slider.classList.remove('show-map'); 946 } 947 }); 948 }); 949 950 951 /* ââ Anchor nav scroll spy + scroll-padding-top åç§»ä¿®æ£ ââ */ 952 const anchorLinks = document.querySelectorAll('.anchor-nav-list a'); 953 if (anchorLinks.length) { 954 const targets = Array.from(anchorLinks).map(function (a) { 955 return document.getElementById(a.getAttribute('href').slice(1)); 956 }).filter(Boolean); 957 958 /* ææªé»æï¼æ²å°æå±¬ section èµ·é»ä¸¦è£ sticky nav é«åº¦ */ 959 anchorLinks.forEach(function (a) { 960 a.addEventListener('click', function (e) { 961 const href = a.getAttribute('href'); 962 if (!href || !href.startsWith('#')) return; 963 const targetEl = document.getElementById(href.slice(1)); 964 if (!targetEl) return; 965 e.preventDefault(); 966 const section = targetEl.closest('section') || targetEl; 967 const nav = document.querySelector('.anchor-nav'); 968 /* ç¨ offsetHeight å nav çæ¸²æé«åº¦ï¼ä¸åæ¯å¦ stickyãæ¯å¦è½å ´ä¸å½±é¿ã 969 滾åå®æå¾ nav ä¸å® sticky å¨ top:0ï¼å¾ä¸æ»¾æèªåé±è headerï¼ï¼ 970 æä»¥ section å°é½ nav åºé¨ = sectionAbsTop - navHeightã */ 971 const navHeight = nav ? nav.offsetHeight : 0; 972 const top = section.getBoundingClientRect().top + window.scrollY - navHeight; 973 window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' }); 974 history.pushState(null, '', href); 975 /* ç«å³æ´æ° active çæ ï¼ä¸ç scroll spy */ 976 anchorLinks.forEach(function (l) { l.classList.remove('active'); }); 977 a.classList.add('active'); 978 }); 979 }); 980 981 function updateAnchorNav() { 982 /* 以å targetãæå±¬ section çé 端ãå¤å® activeï¼å黿æ²åï¼section å°é½ 983 nav åºé¨ï¼ä¸è´ï¼é¾å¼åªé nav é«åº¦ + å°ç·©è¡ï¼é¿å heading å¨ section å § 984 åä¸é æåæ²å°å°±ä»é«äº®åä¸åã */ 985 const nav = document.querySelector('.anchor-nav'); 986 const navHeight = nav ? nav.offsetHeight : 0; 987 const threshold = window.scrollY + navHeight + 12; 988 let active = targets[0]; 989 targets.forEach(function (t) { 990 const section = t.closest('section') || t; 991 if (section.getBoundingClientRect().top + window.scrollY <= threshold) active = t; 992 }); 993 anchorLinks.forEach(function (a) { 994 a.classList.toggle('active', a.getAttribute('href') === '#' + active.id); 995 }); 996 } 997 998 window.addEventListener('scroll', updateAnchorNav, { passive: true }); 999 updateAnchorNav(); 1000 } 1001 1002 /* ââ è²æä¸å¿ï¼ç£å°æ¸ 宿¸²æ ââ */ 1003 const supervisorList = document.getElementById('supervisor-list'); 1004 if (supervisorList && typeof teamData !== 'undefined') { 1005 const supervisors = teamData.filter(function (m) { return m.supervision; }); 1006 1007 if (!supervisors.length) { 1008 supervisorList.innerHTML = 1009 '<p class="supervisor-empty">ç£å°é£å®¹æ´æ°ä¸ï¼æ¡è¿ä¾ä¿¡æ´½è©¢ã</p>'; 1010 } else { 1011 function esc(v) { 1012 return String(v == null ? '' : v) 1013 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 1014 .replace(/"/g, '"'); 1015 } 1016 function field(label, value) { 1017 if (!value) return ''; 1018 return '<div class="supervisor-field">' + 1019 '<p class="supervisor-field-label">' + label + '</p>' + 1020 '<p class="supervisor-field-value">' + esc(value) + '</p>' + 1021 '</div>'; 1022 } 1023 // ç£å°é¢¨æ ¼ï¼åªå style åé ãåèåãçæ¨é¡ï¼å¦ã梳çåæ¡æ¶æ§ãï¼ 1024 function styleTitles(style) { 1025 if (!style) return ''; 1026 const items = ['focus', 'atmosphere', 'transmission', 'gift'] 1027 .map(function (k) { 1028 const v = style[k]; 1029 return v ? String(v).split(/[ï¼:]/)[0].trim() : ''; 1030 }) 1031 .filter(Boolean); 1032 if (!items.length) return ''; 1033 return '<div class="supervisor-field">' + 1034 '<p class="supervisor-field-label">ç£å°é¢¨æ ¼</p>' + 1035 '<ul class="supervisor-style-list">' + 1036 items.map(function (t) { return '<li>' + esc(t) + '</li>'; }).join('') + 1037 '</ul>' + 1038 '</div>'; 1039 } 1040 supervisorList.innerHTML = supervisors.map(function (m) { 1041 const s = m.supervision; 1042 // é åæ²¿ç¨å¾å°è¨å®çå°ç¦é»ï¼æªè¨å®æ photoCropStyle çé è¨ï¼ç½®ä¸è²¼é ï¼ 1043 // è CSS 忬ç object-position ä¸è´ï¼æ ç¡ crop è å¤è§ä¸è®ã 1044 const cropStyle = typeof window.photoCropStyle === 'function' 1045 ? window.photoCropStyle(m.photoCrop) : ''; 1046 const avatar = m.photo
1047 ? '<img class="supervisor-avatar" src="' + esc(m.photo) + '" alt="' + esc(m.name) + 1048 '" loading="lazy" style="' + esc(cropStyle) + '" />' 1049 : '<span class="supervisor-avatar supervisor-avatar--placeholder" aria-hidden="true"></span>'; 1050 const tags = Array.isArray(s.tags) ? s.tags : []; 1051 const groupPlans = Array.isArray(s.groupPlans) ? s.groupPlans : []; 1052 return '' + 1053 '<article class="supervisor-card" id="sv-' + m.id + '" data-tags="' + esc(tags.join('|')) + 1054 '" data-group-plans="' + esc(groupPlans.join('|')) + '">' + 1055 '<div class="supervisor-head">' + 1056 avatar + 1057 '<div class="supervisor-headtext">' + 1058 '<h4 class="supervisor-name">' + esc(m.name) + '</h4>' + 1059 '<p class="supervisor-title">' + esc(m.title) + '</p>' + 1060 '</div>' + 1061 '</div>' + 1062 (s.tagline 1063 ? '<div class="supervisor-field">' + 1064 '<p class="supervisor-field-label">給åç£ççä¸å¥è©±</p>' + 1065 '<p class="supervisor-tagline">ã' + esc(s.tagline) + 'ã</p>' + 1066 '</div>' 1067 : '') + 1068 field('ç£å°åå', s.orientation) + 1069 field('å°é·æåå°è±¡', s.clientGroups) + 1070 styleTitles(s.style) + 1071 '<a class="supervisor-more" href="team-member.html?id=' + m.id + '&view=supervision">ç宿´ä»ç´¹ â</a>' + 1072 '</article>'; 1073 }).join(''); 1074 1075 // ââ å¿«æ·ç¯©é¸æ¨ç±¤ï¼é¡¯ç¤ºå ¨é¨ï¼ä¸åé¡ï¼å®é¸åæï¼ââ 1076 const filterHost = document.getElementById('supervisor-filters'); 1077 if (filterHost) { 1078 const CATS = ['å ¨è·/å ¼è·å¯¦ç¿', 'ç·ä¸é è·', 'æ©æ§/åé«ç£å°']; 1079 let activeTag = null; // null = é¡¯ç¤ºå ¨é¨ 1080 // æ©æ§/åé«ç£å°ç䏿¹æ¡å°å¼å¡ï¼é»æ¹æ¡å¯å篩é¸ä¸æ¹åå®ã 1081 // æ¹æ¡1ï¼æ¹æ¡2ï¼ç¶æé¡¯ç¤ºå ¨é¨æ©æ§/åé«ç£å°çç£å°ï¼æ¹æ¡3ï¼æ©æ§/叿 ¡å¤é¨ 1082 // ç£å°ï¼ï¼åªé¡¯ç¤º supervision.groupPlans ææ¨è¨ãæ¹æ¡3ãçç£å°ã 1083 let activeGroupPlan = null; // null = ä¸éæ¹æ¡ 1084 const groupGuideBadges = document.querySelectorAll('.group-guide-badge[data-group-plan]'); 1085 function renderGroupGuideBadges() { 1086 groupGuideBadges.forEach(function (btn) { 1087 const on = activeGroupPlan === btn.getAttribute('data-group-plan'); 1088 btn.setAttribute('aria-pressed', on ? 'true' : 'false'); 1089 }); 1090 } 1091 function renderChips() { 1092 const chips = [{ v: null, label: 'é¡¯ç¤ºå ¨é¨' }] 1093 .concat(CATS.map(function (c) { return { v: c, label: c }; })); 1094 filterHost.innerHTML = chips.map(function (c) { 1095 const on = activeTag === c.v; 1096 return '<button type="button" class="supervisor-chip' + (on ? ' is-active' : '') + 1097 '" data-tag="' + (c.v == null ? '' : esc(c.v)) + '"' + 1098 (on ? ' aria-pressed="true"' : ' aria-pressed="false"') + '>' + 1099 esc(c.label) + '</button>'; 1100 }).join(''); 1101 } 1102 function applyFilter() { 1103 // æ©æ§/åé«ç£å°ï¼æ¼åå®ä¸æ¹é¡¯ç¤ºä¸æ¬å¼å°å¼å¡ 1104 const guide = document.getElementById('group-supervision-guide'); 1105 if (guide) guide.hidden = activeTag !== 'æ©æ§/åé«ç£å°'; 1106 renderGroupGuideBadges(); 1107 const cards = supervisorList.querySelectorAll('.supervisor-card'); 1108 let shown = 0; 1109 cards.forEach(function (card) { 1110 const tags = (card.getAttribute('data-tags') || '').split('|').filter(
1110Boolean); 1111 const groupPlans = (card.getAttribute('data-group-plans') || '').split('|').filter(Boolean); 1112 const matchTag = !activeTag || tags.indexOf(activeTag) !== -1; 1113 const matchGroupPlan = !activeGroupPlan || activeGroupPlan !== 'æ¹æ¡3' || 1114 groupPlans.indexOf('æ¹æ¡3') !== -1; 1115 const match = matchTag && matchGroupPlan; 1116 card.hidden = !match; 1117 if (match) shown++; 1118 }); 1119 let empty = supervisorList.querySelector('.supervisor-filter-empty'); 1120 if (!shown) { 1121 if (!empty) { 1122 empty = document.createElement('p'); 1123 empty.className = 'supervisor-empty supervisor-filter-empty'; 1124 supervisorList.appendChild(empty); 1125 } 1126 empty.textContent = 'ç®åæ²æç¬¦åæ¤åé¡çç£å°ï¼è©¦èåæå ¶ä»æ¨ç±¤ã'; 1127 empty.hidden = false; 1128 } else if (empty) { 1129 empty.hidden = true; 1130 } 1131 } 1132 filterHost.addEventListener('click', function (e) { 1133 const chip = e.target.closest('.supervisor-chip'); 1134 if (!chip) return; 1135 const v = chip.getAttribute('data-tag'); 1136 activeTag = v ? v : null; 1137 activeGroupPlan = null; // åæä¸»åé¡æï¼æ¹æ¡å篩é¸éç½® 1138 renderChips(); 1139 applyFilter(); 1140 }); 1141 groupGuideBadges.forEach(function (btn) { 1142 btn.addEventListener('click', function () { 1143 const plan = btn.getAttribute('data-group-plan'); 1144 activeGroupPlan = activeGroupPlan === plan ? null : plan; 1145 applyFilter(); 1146 supervisorList.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1147 }); 1148 }); 1149 renderChips(); 1150 } 1151 } 1152 1153 // åæ æ¸²æå¾æèçé¨é»ï¼åç hash æå¨å¡çç¢çå失æï¼æ æåæ²å 1154 if (window.location.hash) { 1155 const target = document.getElementById(window.location.hash.slice(1)); 1156 if (target && target.classList.contains('supervisor-card')) { 1157 target.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1158 target.classList.add('supervisor-card--highlight'); 1159 setTimeout(function () { 1160 target.classList.remove('supervisor-card--highlight'); 1161 }, 1800); 1162 } 1163 } 1164 } 1165 1166 // ââ éå ´æé Aï¼å¹³æ»æ²åå°é£å®¹ç¯©é¸åï¼æ£æ sticky nav é«åº¦ï¼ââ 1167 const routeBrowse = document.getElementById('supervision-route-browse'); 1168 if (routeBrowse) { 1169 routeBrowse.addEventListener('click', function (e) { 1170 const target = document.getElementById('supervisor-filters') || 1171 document.getElementById('supervisor-list'); 1172 if (!target) return; // æ¾ä¸å°å°±éååçé¨é» 1173 e.preventDefault(); 1174 const nav = document.querySelector('.anchor-nav'); 1175 const navH = nav ? nav.offsetHeight : 0; 1176 const top = target.getBoundingClientRect().top + window.scrollY - navH - 16; 1177 window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' }); 1178 }); 1179 } 1180 1181})(); 1182 1183/* ============================================================ 1184 è²æä¸å¿ï¼èª²ç¨åï¼æ¬å£ä¸»é¡ç½®é ï¼ å¯ç¯©é¸èª²ç¨åè¡¨ï¼ 1185 ============================================================ */ 1186(function () { 1187 var listEl = document.getElementById('course-list'); 1188 if (!listEl || typeof courseData === 'undefined') return; 1189 1190 function esc(v) { 1191 return String(v == null ? '' : v) 1192 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 1193 .replace(/"/g, '"'); 1194 } 1195 function asArray(v) { return Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]); } 1196 function displayAudience(v) { return v === 'å®¶é·/深度å¸ç¿è ' ? 'å®¶é·ã»æ·±åº¦å¸ç¿è ' : v; } 1197 1198 // çæ â CSS 修飾å 1199 var STATUS_MOD = { 'ç¾æ£æç': 'open', 'å³å°æ¨åº': 'soon', 'å ¸è課ç¨': 'archive' }; 1200 // å½¢å¼ï¼é éæ¹å¼ï¼æ¨ç±¤ 1201 function formatTags(course) { 1202 return asArray(course.format).map(function (f) { 1203 return '<span class="course-tag course-tag--format">' + esc(f) + '</span>'; 1204 }).join(''); 1205 } 1206 function statusTags(course) { 1207 var out = ''; 1208 if (course.hot) out += '<span class="course-tag course-tag--hot">
1208ç±é課ç¨</span>'; 1209 if (course.status) { 1210 out += '<span class="course-tag course-tag--status is-' + 1211 (STATUS_MOD[course.status] || 'open') + '">' + esc(course.status) + '</span>'; 1212 } 1213 return out; 1214 } 1215 1216 function courseCover(course) { 1217 if (course.cover) { 1218 return '<div class="course-cover" style="background-image:url(' + esc(course.cover) + ')"></div>'; 1219 } 1220 // ä½ä½ï¼å£åº¦ï¼ç³»åæå 1221 return '<div class="course-cover course-cover--placeholder">' + 1222 '<span class="course-cover-series">' + esc(course.series || '課ç¨') + '</span>' + 1223 (course.season ? '<span class="course-cover-season">' + esc(course.season) + '</span>' : '') + 1224 '</div>'; 1225 } 1226 1227 // ç± instructorMemberId å°æåéæå¡ç §ç 1228 function instructorPhoto(course) { 1229 if (!course.instructorMemberId || typeof teamData === 'undefined') return ''; 1230 var m = teamData.filter(function (t) { return t.id === course.instructorMemberId; })[0]; 1231 return m && m.photo ? m.photo : ''; 1232 } 1233 1234 function courseInstructor(course) { 1235 if (!course.instructor) return ''; 1236 var photo = instructorPhoto(course); 1237 var avatar = photo 1238 ? '<img class="course-instructor-avatar" src="' + esc(photo) + '" alt="' + esc(course.instructor) + '" loading="lazy" />' 1239 : '<span class="course-instructor-avatar course-instructor-avatar--placeholder" aria-hidden="true"></span>'; 1240 var text = esc(course.instructor) + (course.hours ? '<span class="course-instructor-hours">ï½' + esc(course.hours) + '</span>' : ''); 1241 return '<p class="course-card-instructor">' + avatar + 1242 '<span class="course-instructor-text">' + text + '</span></p>'; 1243 } 1244 1245 function courseCard(course) { 1246 return '' + 1247 '<a class="course-card" href="course.html?id=' + esc(course.id) + '">' + 1248 courseCover(course) + 1249 '<div class="course-card-body">' + 1250 (course.series ? '<span class="course-card-series">' + esc(course.series) + '</span>' : '') + 1251 '<h4 class="course-card-title">' + esc(course.title) + '</h4>' + 1252 courseInstructor(course) + 1253 '<div class="course-tags">' + formatTags(course) + statusTags(course) + '</div>' + 1254 (course.enrollDeadline ? '<p class="course-card-deadline">å ±åæªæ¢ ' + esc(course.enrollDeadline) + '</p>' : '') + 1255 '</div>' + 1256 '</a>'; 1257 } 1258 1259 /* ââ æ¬å£ä¸»é¡ç½®é ââ */ 1260 var hasSeason = typeof seasonMeta !== 'undefined'; 1261
1262 (function renderSeason() { 1263 var host = document.getElementById('season-feature'); 1264 if (!host || !hasSeason) return; 1265 var sm = seasonMeta; 1266 var coverAlt = esc(sm.name) + ' 主è¦è¦º'; 1267 host.innerHTML = 1268 (sm.cover ? '<button type="button" class="season-feature-cover" data-poster-src="' + esc(sm.cover) + '" data-poster-alt="' + coverAlt + '" aria-label="æ¾å¤§æª¢è¦ ' + coverAlt + '">' + 1269 '<img src="' + esc(sm.cover) + '" alt="' + coverAlt + '" loading="lazy" />' + 1270 '<span class="season-feature-zoom" aria-hidden="true">黿ç大å</span>' + 1271 '</button>' : '') + 1272 '<div class="season-feature-head">' + 1273 '<span class="season-badge">æ¬å£ä¸»é¡</span>' + 1274 '<h3 class="season-title">' + esc(sm.season) + 'ï½' + esc(sm.name) + '</h3>' + 1275 (sm.theme ? '<p class="season-theme">' + esc(sm.theme) + '</p>' : '') + 1276 (sm.tagline ? '<p class="season-intro">' + esc(sm.tagline) + '</p>' : '') + 1277 '</div>' + 1278 '<button type="button" class="season-cta" id="season-view-courses">æ¥çæ¬å£èª²ç¨ â</button>'; 1279 1280 var coverBtn = host.querySelector('.season-feature-cover'); 1281 if (coverBtn) { 1282 coverBtn.addEventListener('click', function () { 1283 openPosterLightbox(coverBtn.getAttribute('data-poster-src'), coverBtn.getAttribute('data-poster-alt')); 1284 }); 1285 } 1286 })(); 1287 1288 /* ââ æµ·å ±çç®±ï¼é»æç宿´å¤§åï¼ââ */ 1289 function openPosterLightbox(src, alt) { 1290 if (!src) return; 1291 var lb = document.getElementById('poster-lightbox'); 1292 if (!lb) { 1293 lb = document.createElement('div'); 1294 lb.id = 'poster-lightbox'; 1295 lb.className = 'poster-lightbox'; 1296 lb.setAttribute('role', 'dialog'); 1297 lb.setAttribute('aria-modal', 'true'); 1298 lb.setAttribute('aria-label', 'èª²ç¨æµ·å ±'); 1299 lb.innerHTML = 1300 '<button type="button" class="poster-lightbox-close" aria-label="éé">Ã</button>' + 1301 '<img class="poster-lightbox-img" alt="" />'; 1302 document.body.appendChild(lb); 1303 var close = function () { 1304 lb.classList.remove('open'); 1305 document.body.classList.remove('poster-lightbox-open'); 1306 }; 1307 lb.querySelector('.poster-lightbox-close').addEventListener('click', close); 1308 lb.addEventListener('click', function (e) { if (e.target === lb) close(); }); 1309 document.addEventListener('keydown', function (e) { 1310 if (e.key === 'Escape' && lb.classList.contains('open')) close(); 1311 }); 1312 } 1313 var img = lb.querySelector('.poster-lightbox-img'); 1314 img.src = src; 1315 img.alt = alt || 'èª²ç¨æµ·å ±'; 1316 lb.classList.add('open'); 1317 document.body.classList.add('poster-lightbox-open'); 1318 lb.querySelector('.poster-lightbox-close').focus(); 1319 } 1320 1321 /* ââ ç¯©é¸ ââ */ 1322 var FILTERS = [ 1323 { key: 'audience', label: 'å°è±¡', options: ['å°æ¥äººå¡', 'å®¶é·/深度å¸ç¿è '] }, 1324 { key: 'topics', label: '主é¡', options: ['å ç«¥ç¼å±', 'è¦ªè·æé¤', 'åµå·', 'æ ç·', 'éä¿'] }, 1325 { key: 'series', label: 'ç³»å', options: ['éå£è¬åº§', '大ç¾ï¼å®¶é·ç³»å', 'å°æ¥é²ä¿®ç³»å', '深度äºåç³»å'] }, 1326 ]; 1327 var active = { audience: [], topics: [], series: [], season: null }; 1328 1329 function anyActive() { 1330 return FILTERS.some(function (g) { return active[g.key].length; }) || !!active.season; 1331 } 1332 1333 function renderFilters() { 1334 var host = document.getElementById('course-filters'); 1335 if (!host) return; 1336 var groupsHtml = FILTERS.map(function (g) { 1337 var chips = g.options.map(function (opt) { 1338 var on = active[g.key].indexOf(opt) !== -1; 1339 return '<button type="button" class="course-chip' + (on ? ' is-active' : '') + 1340 '" data-group="' + g.key + '" data-value="' + esc(opt) + '"' + 1341 (on ? ' aria-pressed="true"' : ' aria-pressed="false"') + '>' + 1342 esc(displayAudience(opt)) + '</button>'; 1343 }).join(''); 1344 return '<div class="course-filter-group">' + 1345 '<span class="course-filter-label">' + esc(g.label) + '</span>' + 1346 '<div class="course-filter-chips">' + chips + '</div>' + 1347 '</div>'; 1348 }).join('');
1349 // æ¬å£ä¸»é¡ä½ç¨ä¸ï¼å¯ç§»é¤æ¨ç±¤ 1350 var seasonHtml = active.season 1351 ? '<div class="course-filter-active"><button type="button" class="active-season-chip" id="course-season-clear">æ¬å£ä¸»é¡ï¼' + 1352 esc(hasSeason ? seasonMeta.name : active.season) + ' â</button></div>' 1353 : ''; 1354 var clearHtml = anyActive() 1355 ? '<button type="button" class="course-filter-clear" id="course-filter-clear">æ¸ é¤ç¯©é¸ â</button>' 1356 : ''; 1357 host.innerHTML = seasonHtml + groupsHtml + clearHtml; 1358 } 1359 1360 function matches(course) { 1361 if (active.season && course.season !== active.season) return false; 1362 return FILTERS.every(function (g) { 1363 var sel = active[g.key]; 1364 if (!sel.length) return true; 1365 var vals = asArray(course[g.key]); 1366 return sel.some(function (s) { return vals.indexOf(s) !== -1; }); 1367 }); 1368 } 1369 1370 function renderList() { 1371 var shown = courseData.filter(matches); 1372 var countEl = document.getElementById('course-count'); 1373 if (countEl) countEl.textContent = 'å ± ' + shown.length + ' å '; 1374 if (!shown.length) { 1375 listEl.innerHTML = '<p class="course-empty">ç®åæ²æç¬¦åæ¢ä»¶ç課ç¨ï¼è©¦è調æ´ç¯©é¸æ¢ä»¶ã</p>'; 1376 return; 1377 } 1378 listEl.innerHTML = shown.map(courseCard).join(''); 1379 } 1380 1381 function scrollToCatalog() { 1382 var target = document.getElementById('course-catalog') || listEl; 1383 var nav = document.querySelector('.anchor-nav'); 1384 var navH = nav ? nav.offsetHeight : 0; 1385 var top = target.getBoundingClientRect().top + window.scrollY - navH - 12; 1386 window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' }); 1387 } 1388 1389 document.getElementById('course-filters').addEventListener('click', function (e) { 1390 var chip = e.target.closest('.course-chip'); 1391 if (chip) { 1392 var group = chip.getAttribute('data-group'); 1393 var value = chip.getAttribute('data-value'); 1394 var arr = active[group]; 1395 var i = arr.indexOf(value); 1396 if (i === -1) arr.push(value); else arr.splice(i, 1); 1397 renderFilters(); 1398 renderList(); 1399 return; 1400 } 1401 if (e.target.closest('#course-season-clear')) { 1402 active.season = null; 1403 renderFilters(); 1404 renderList(); 1405 return; 1406 } 1407 if (e.target.closest('#course-filter-clear')) { 1408 active = { audience: [], topics: [], series: [], season: null }; 1409 renderFilters(); 1410 renderList(); 1411 } 1412 }); 1413 1414 // ãæ¥çæ¬å£èª²ç¨ãï¼å¥ç¨æ¬å£ç¯©é¸ä¸¦æ²åå°èª²ç¨å表 1415 var seasonBtn = document.getElementById('season-view-courses'); 1416 if (seasonBtn && hasSeason) { 1417 seasonBtn.addEventListener('click', function () { 1418 active.season = seasonMeta.currentSeason; 1419 renderFilters(); 1420 renderList(); 1421 scrollToCatalog(); 1422 }); 1423 } 1424 1425 renderFilters(); 1426 renderList(); 1427})(); 1428 1429/* ============================================================ 1430 è²æä¸å¿ï¼èª²ç¨è©³æ é ï¼course.html?id=ï¼ 1431 ============================================================ */ 1432(function () { 1433 var host = document.getElementById('course-detail'); 1434 if (!host || typeof courseData === 'undefined') return; 1435 1436 function esc(v) { 1437 return String(v == null ? '' : v) 1438 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 1439 .replace(/"/g, '"'); 1440 } 1441 function asArray(v) { return Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]); } 1442 function displayAudience(v) { return v === 'å®¶é·/深度å¸ç¿è ' ? 'å®¶é·ã»æ·±åº¦å¸ç¿è ' : v; } 1443 1444 var params = new URLSearchParams(window.location.search); 1445 var id = params.get('id'); 1446 var course = courseData.filter(function (c) { return c.id === id; })[0]; 1447 1448 if (!course) { 1449 host.innerHTML = 1450 '<div class="course-notfound">' + 1451 '<h1>æ¾ä¸å°éé課ç¨</h1>' + 1452 '<p>課ç¨å¯è½å·²ä¸æ¶æé£çµæèª¤ã</p>' + 1453 '<a href="yucai.html#courses-heading" class="btn btn-primary">åå°è²æä¸å¿èª²ç¨</a>' + 1454 '</div>'; 1455 return; 1456 } 1457 1458 document.title = course.title + ' â ç±³é²è°·è²æä¸å¿'; 1459 1460 // å享網åï¼åªå ç¨å享åç¶²å /c/:idï¼æå°å±¬ OG é 覽ï¼ï¼æªè¨å®åé忬é ç¶²å 1461 var shareBase = (window.MIRUKU_SHARE_BASE || '').replace(/\/+$/, ''); 1462 var shareUrl = shareBase 1463 ? shareBase + '/s/' + encodeURIComponent(id) 1464 : window.location.origin + '/course.html?id=' + encodeURIComponent(id); 1465 function shareBar() { 1466 var u = encodeURIComponent(shareUrl); 1467 var line = 'https://social-plugins.line.me/lineit/share?url=' + u; 1468 var fb = 'https://www.facebook.com/sharer/sharer.php?u=' + u; 1469 var ICON_LINE = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12.007 2.07c-5.458 0-9.93 3.952-9.93 8.814 0 4.358 3.86 8.007 9.073 8.698.353.076.833.233.954.534.11.274.072.703.035.98l-.155.925c-.047.274-.218 1.073.94.585 1.157-.488 6.246-3.678 8.522-6.296 1.572-1.724 2.327-3.47 2.327-5.426.001-4.862-4.47-8.814-9.766-8.814zm-2.922 11.4H7.216a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h.583c.322 0 .583.261.583.583v3.76h1.703c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583zm2.339-.583a.583.583 0 0 1-.583.583h-.583a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h.583c.322 0 .583.261.583.583v4.343zm5.133 0a.583.583 0 0 1-.583.583h-.583a.584.584 0 0 1-.476-.244l-1.896-2.56v2.22a.583.583 0 0 1-.583.584h-.583a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h.583c.187 0 .35.085.46.217l1.912 2.587V8.544c0-.322.261-.583.583-.583h.583c.322 0 .583.261.583.583v4.343zm2.922.583h-1.869a.583.583 0 0 1-.583-.583V8.544c0-.322.261-.583.583-.583h1.869c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583h-1.286v.876h1.286c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583h-1.286v.877h1.28
14696c.322 0 .583.261.583.583v.583a.583.583 0 0 1-.583.583z"/></svg>'; 1470 var ICON_FB = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07c0 6.02 4.39 11.01 10.13 11.93v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8v8.44C19.61 23.08 24 18.09 24 12.07z"/></svg>'; 1471 var ICON_COPY = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg>'; 1472 return '<div class="course-share">' + 1473 '<span class="course-share-label">å享</span>' + 1474 '<div class="course-share-btns">' + 1475 '<a class="course-share-btn is-line" href="' + line + '" target="_blank" rel="noopener" aria-label="åäº«å° LINE" title="åäº«å° LINE">' + ICON_LINE + '</a>' + 1476 '<a class="course-share-btn is-fb" href="' + fb + '" target="_blank" rel="noopener" aria-label="åäº«å° Facebook" title="åäº«å° Facebook">' + ICON_FB + '</a>' + 1477 '<button type="button" class="course-share-btn is-copy" data-share-url="' + esc(shareUrl) + '" aria-label="è¤è£½èª²ç¨é£çµ" title="è¤è£½èª²ç¨é£çµ">' + ICON_COPY + '</button>' + 1478 '</div>' + 1479 '</div>'; 1480 } 1481 1482 var STATUS_MOD = { 'ç¾æ£æç': 'open', 'å³å°æ¨åº': 'soon', 'å ¸è課ç¨': 'archive' }; 1483 function tags() { 1484 var out = asArray(course.format).map(function (f) { 1485 return '<span class="course-tag course-tag--format">' + esc(f) + '</span>'; 1486 }).join(''); 1487 if (course.hot) out += '<span class="course-tag course-tag--hot">ç±é課ç¨</span>'; 1488 if (course.status) { 1489 out += '<span class="course-tag course-tag--status is-' + 1490 (STATUS_MOD[course.status] || 'open') + '">' + esc(course.status) + '</span>'; 1491 } 1492 return out; 1493 } 1494 1495 // åå¡ï¼æ¨é¡ + å §å®¹ï¼å §å®¹ç©ºååå³ ''ï¼èªåé±èï¼ 1496 function block(title, inner) { 1497 if (!inner) return ''; 1498 return '<section class="course-block">' + 1499 '<h2 class="course-block-title">' + esc(title) + '</h2>' + inner + '</section>'; 1500 } 1501 function infoRow(label, value) { 1502 if (!value) return ''; 1503 return '<div class="course-info-row">' + 1504 '<span class="course-info-label">' + esc(label) + '</span>' + 1505 '<span class="course-info-value">' + esc(value) + '</span></div>'; 1506 } 1507 function bullets(arr, cls) { 1508 var items = asArray(arr); 1509 if (!items.length) return ''; 1510 return '<ul class="' + cls + '">' + items.map(function (t) { 1511 return '<li>' + esc(t) + '</li>'; 1512 }).join('') + '</ul>'; 1513 } 1514 1515 // 1. å°é¢ 1516 var cover = course.cover 1517 ? '<div class="course-hero-cover"><img src="' + esc(course.cover) + '" alt="' + esc(course.title) + ' èª²ç¨æµ·å ±" /></div>' 1518 : '<div class="course-hero-cover course-hero-cover--placeholder">' + 1519 '<span>' + esc(course.series || '課ç¨') + '</span></div>'; 1520 1521 var heroHtml = 1522 '<header class="course-hero">' + 1523 cover + 1524 '<div class="course-hero-text">' + 1525 (course.season ? '<span class="course-hero-season">' + esc(course.season) + '</span>' : '') + 1526 '<div class="course-hero-head">' + 1527 '<h1 class="course-hero-title">' + esc(course.title) + '</h1>' + 1528 shareBar() + 1529 '</div>' + 1530 '<div class="course-tags course-hero-tags">' + tags() + '</div>' + 1531 '</div>' + 1532 '</header>'; 1533 1534 // 2. 課ç¨è³è¨ 1535 // è¬å¸«è¥çºæ¬æåéæå¡ï¼instructorMemberIdï¼ï¼å§åé£å°å ¶å人é ï¼ä¸¦å試ååå ¶åéè³æï¼é å/è·ç¨±ï¼ 1536 var memberId = course.instructorMemberId; 1537 var member = (memberId && typeof teamData !== 'undefined') 1538 ? teamData.filter(function (m) { return m.id === memberId; })[0] : null; 1539 var memberHref = memberId ? 'team-member.html?id=' + esc(memberId) : ''; 1540 var instructorRow = memberId && course.instructor 1541 ? '<div class="course-info-row"><span class="course-info-label">è¬å¸«</span>' + 1542 '<span class="course-info-value"><a class="course-instructor-link" href="' + memberHref + '">' + esc(course.instructor) + '</a></span></div>' 1543 : infoRow('è¬å¸«', course.instructor); 1544 var infoHtml = 1545 instructorRow + 1546 infoRow('課ç¨å°è±¡', asArray(course.audience).map(displayAudience).join('ã')) + 1547 infoRow('課ç¨å½¢å¼', asArray(course.format).join('ã')) + 1548 infoRow('ä¸èª²ææ®µ', course.schedule || course.startDate) + 1549 infoRow('ä¸èª²å°é»', course.location) + 1550 infoRow('人æ¸', course.capacity) + 1551 infoRow('è²»ç¨', course.price) + 1552 infoRow('å ±åæªæ¢', course.enrollDeadline) + 1553 infoRow('ç ç¿èæ', course.certificate); 1554 var infoBlock = infoHtml 1555 ? '<section class="course-block"><h2 class="course-block-title">課ç¨è³è¨</h2>' + 1556 '<div class="course-info">' + infoHtml + '</div></section>' 1557 : ''; 1558 1559 // 3. ç°¡ä» 1560 var introBlock = block('課ç¨ç°¡ä»', 1561 course.intro ? '<p class="course-intro">' + esc(course.intro) + '</p>' : ''); 1562 1563 // 4. ç¹è² 1564 var featureBlock = block('課ç¨ç¹è²', bullets(course.features, 'course-features')); 1565 1566 // 5. è¬å¸«è³è¨ 1567 var bioInner = ''; 1568 if (memberId) { 1569 // è¬å¸«çºæ¬æåéæå¡ï¼æ´å¼µå¡çå¯é»ï¼é£å°å人é ï¼é å/è·ç¨±åèªåéè³æï¼ 1570 var cardName = esc(member && member.name ? member.name : course.instructor); 1571 var cardTitle = member
1572 ? [member.title, member.role].filter(Boolean).map(esc).join('ï½') 1573 : 'ç±³é²è°·åéæå¡'; 1574 var avatar = (member && member.photo) 1575 ? '<span class="course-bio-avatar" style="background-image:url(' + esc(member.photo) + ')"></span>' 1576 : '<span class="course-bio-avatar course-bio-avatar--empty"></span>'; 1577 bioInner = 1578 '<a class="course-bio-card" href="' + memberHref + '">' + 1579 avatar + 1580 '<span class="course-bio-cardinfo">' + 1581 '<span class="course-bio-cardname">' + cardName + '</span>' + 1582 (cardTitle ? '<span class="course-bio-cardtitle">' + cardTitle + '</span>' : '') + 1583 '</span>' + 1584 '<span class="course-bio-cardbtn">æ¥ç宿´ä»ç´¹ â</span>' + 1585 '</a>'; 1586 } else { 1587 var bio = course.instructorBio || {}; 1588 var bioRows = 1589 infoRow('å¸ç¶æ·', bio.background) + 1590 infoRow('å°é·', bio.expertise) + 1591 infoRow('æ é·ä¸»é¡', bio.topics) + 1592 infoRow('æ¾é課ç¨', bio.pastCourses); 1593 if (bioRows || bio.philosophy) { 1594 bioInner = '<div class="course-bio">' + 1595 (course.instructor ? '<p class="course-bio-name">' + esc(course.instructor) + '</p>' : '') + 1596 (bioRows ? '<div class="course-info">' + bioRows + '</div>' : '') + 1597 (bio.philosophy ? '<blockquote class="course-bio-quote">ã' + esc(bio.philosophy) + 'ã</blockquote>' : '') + 1598 '</div>'; 1599 } 1600 } 1601 var bioBlock = block('è¬å¸«è³è¨', bioInner); 1602 1603 // 6. é©åå°è±¡ 1604 var suitableBlock = block('é©åå°è±¡', bullets(course.suitableFor, 'course-suitable')); 1605 1606 // 7. èª²ç¨ææ 1607 var outcomesBlock = block('èª²ç¨ææ', 1608 course.outcomes ? '<p class="course-intro">' + esc(course.outcomes) + '</p>' : ''); 1609 1610 // 8. å¸å¡åé¥ 1611 var testis = asArray(course.testimonials); 1612 var testiInner = testis.length 1613 ? '<div class="course-testimonials">' + testis.map(function (t) { 1614 var text = typeof t === 'string' ? t : (t.text || ''); 1615 var from = typeof t === 'object' && t.from ? t.from : ''; 1616 if (!text) return ''; 1617 return '<blockquote class="course-testimonial">' + 1618 '<p>ã' + esc(text) + 'ã</p>' + 1619 (from ? '<cite>â ' + esc(from) + '</cite>' : '') + 1620 '</blockquote>'; 1621 }).join('') + '</div>' 1622 : ''; 1623 var testiBlock = block('å¸å¡åé¥', testiInner); 1624 1625 // 9. ç¸éèª²ç¨ 1626 var relIds = asArray(course.related); 1627 var relCourses = relIds.map(function (rid) { 1628 return courseData.filter(function (c) { return c.id === rid; })[0]; 1629 }).filter(Boolean); 1630 var relInner = relCourses.length 1631 ? '<div class="course-related">' + relCourses.map(function (c) { 1632 return '<a class="course-related-card" href="course.html?id=' + esc(c.id) + '">' + 1633 (c.series ? '<span class="course-card-series">' + esc(c.series) + '</span>' : '') + 1634 '<span class="course-related-title">' + esc(c.title) + '</span>' + 1635 '<span class="course-related-arrow" aria-hidden="true">â</span>' + 1636 '</a>'; 1637 }).join('') + '</div>' 1638 : ''; 1639 var relBlock = block('ç¸é課ç¨', relInner); 1640 1641 // å ±åèéè²»è¦ç¯ï¼refund å ¨æï¼æè¡ä¿çï¼ç©ºè¡å段ãå®ä¸æè¡ â <br>ï¼ 1642 function multiline(text) { 1643 return String(text).split(/\n{2,}/).map(function (p) { 1644 return '<p>' + esc(p).replace(/\n/g, '<br>') + '</p>'; 1645 }).join(''); 1646 } 1647 var refundBlock = block('å ±åèéè²»è¦ç¯', 1648 course.refund ? '<div class="course-refund">' + multiline(course.refund) + '</div>' : ''); 1649 1650 // CTAï¼ä¾çæ èª¿æ´ææ¡ï¼ 1651 var ctaText, ctaNote; 1652 if (course.status === 'å ¸è課ç¨') { 1653 ctaText = '洽詢æªä¾å ´æ¬¡'; ctaNote = 'æ¬æ¢¯æ¬¡å·²çµæï¼æ¡è¿ä¾ä¿¡è©¢åæªä¾é課è³è¨ã'; 1654 } else if (course.status === 'å³å°æ¨åº') { 1655 ctaText = 'æè¦æ´½è©¢'; ctaNote = 'èª²ç¨æºåä¸ï¼æ¡è¿å ä¾ä¿¡æ´½è©¢ï¼éæ¾å ±åæå°åªå éç¥ã'; 1656 } else { 1657 ctaText = 'æè¦å ±å / 洽詢'; ctaNote = 'å ±åè課ç¨ç¸éåé¡ï¼æ¡è¿ééè¯çµ¡é 颿 LINE èæåè¯ç¹«ã'; 1658 } 1659 var enrollIsUrl = /^https?:\/\//i.test(course.enroll || ''); 1660 var primaryBtn = (enrollIsUrl && course.status === 'ç¾æ£æç') 1661 ? '<a href="' + esc(course.enroll) + '" target="_blank" rel="noopener" class="btn btn-primary">åå¾å ±å â</a>' 1662 : '<a href="contact.html" class="btn btn-primary">' + esc(ctaText) + '</a>'; 1663 var enrollNote = (!enrollIsUrl && course.enroll) 1664 ? '<p class="course-cta-enroll">' + esc(course.enroll) + '</p>' : ''; 1665 var ctaHtml = 1666 '<div class="course-cta">' + 1667 '<p class="course-cta-note">' + esc(ctaNote) + '</p>' + 1668 enrollNote + 1669 '<div class="course-cta-btns">' + 1670 primaryBtn + 1671 '<a href="yucai.html#courses-heading" class="btn btn-outline">åå°èª²ç¨å表</a>' + 1672 '</div>' + 1673 '</div>'; 1674 1675 host.innerHTML = 1676 heroHtml + infoBlock + introBlock + featureBlock + bioBlock + 1677 suitableBlock + outcomesBlock + testiBlock + relBlock + refundBlock + ctaHtml; 1678 1679 // è¤è£½é£çµ 1680 var copyBtn = host.querySelector('.course-share-btn.is-copy'); 1681 if (copyBtn) { 1682 var copyOrig = copyBtn.innerHTML; 1683 var ICON_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>'; 1684 copyBtn.addEventListener('click', function () { 1685 var t = copyBtn.getAttribute('data-share-url'); 1686 if (navigator.clipboard && navigator.clipboard.writeText) { 1687 navigator.clipboard.writeText(t).then(function () { 1688 copyBtn.innerHTML = ICON_CHECK; 1689 copyBtn.classList.add('is-copied'); 1690 copyBtn.setAttribute('title', 'å·²è¤è£½ â'); 1691 setTimeout(function () { 1692 copyBtn.innerHTML = copyOrig; 1693 copyBtn.classList.remove('is-copied'); 1694 copyBtn.setAttribute('title', 'è¤è£½èª²ç¨é£çµ'); 1695 }, 1600); 1696 }); 1697 } else { 1698 window.prompt('è¤è£½æ¤é£çµï¼', t); 1699 } 1700 }); 1701 } 1702})(); 1703 1704/* ============================================================ 1705 Hero blob â SVG path morph + entrance animation 1706 ============================================================ */ 1707(function () { 1708 var svgs = document.querySelectorAll('.hero [data-blob]'); 1709 if (!svgs.length) return; 1710 1711 var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1712 1713 function blobPath(cx, cy, r, n, amp, t, phase) { 1714 var pts = []; 1715 for (var i = 0; i < n; i++) { 1716 var a = (i / n) * Math.PI * 2; 1717 var radius = r * (1 + amp * Math.sin(t + phase + i * 1.7));
1718 pts.push({ x: cx + Math.cos(a) * radius, y: cy + Math.sin(a) * radius }); 1719 } 1720 var d = 'M ' + pts[0].x.toFixed(2) + ',' + pts[0].y.toFixed(2); 1721 for (var j = 0; j < n; j++) { 1722 var cur = pts[j]; 1723 var next = pts[(j + 1) % n]; 1724 var prev = pts[(j - 1 + n) % n]; 1725 var nn = pts[(j + 2) % n]; 1726 var c1x = cur.x + (next.x - prev.x) / 6; 1727 var c1y = cur.y + (next.y - prev.y) / 6; 1728 var c2x = next.x - (nn.x - cur.x) / 6; 1729 var c2y = next.y - (nn.y - cur.y) / 6; 1730 d += ' C ' + c1x.toFixed(2) + ',' + c1y.toFixed(2) 1731 + ' ' + c2x.toFixed(2) + ',' + c2y.toFixed(2) 1732 + ' ' + next.x.toFixed(2) + ',' + next.y.toFixed(2); 1733 } 1734 return d + ' Z'; 1735 } 1736 1737 var blobs = Array.prototype.map.call(svgs, function (svg) { 1738 return { 1739 path: svg.querySelector('path'), 1740 cx: +svg.dataset.cx, cy: +svg.dataset.cy, 1741 r: +svg.dataset.r, n: +svg.dataset.n, 1742 amp: +svg.dataset.amp, 1743 speed: +svg.dataset.speed, 1744 phase: +svg.dataset.phase 1745 }; 1746 }); 1747 1748 /* Seed an initial path so blob isn't blank during fade-in */ 1749 blobs.forEach(function (b) { 1750 b.path.setAttribute('d', blobPath(b.cx, b.cy, b.r, b.n, b.amp, 0, b.phase)); 1751 }); 1752 1753 if (!reduceMotion) { 1754 var start = performance.now(); 1755 var tick = function (now) { 1756 var elapsed = (now - start) / 1000; 1757 blobs.forEach(function (b) { 1758 b.path.setAttribute('d', blobPath(b.cx, b.cy, b.r, b.n, b.amp, elapsed * b.speed, b.phase)); 1759 }); 1760 requestAnimationFrame(tick); 1761 }; 1762 requestAnimationFrame(tick); 1763 } 1764 1765 /* Entrance â toggle on next frame so CSS transition fires */ 1766 requestAnimationFrame(function () { 1767 requestAnimationFrame(function () { 1768 document.querySelectorAll('.hero .hero-blob-layer, .hero .hero-card') 1769 .forEach(function (el) { el.classList.add('is-in'); }); 1770 }); 1771 }); 1772})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.