1 (function() { 2 'use strict'; 3 // ==================== å ±éã³ã¢ãåå²ä»£å ¥ï¼kaitorix-core.js ãåä¸ççæºï¼ ==================== 4 // AMAZON_TAG/CONFIG/STORE_COLORS/KX_STORE_MAP/deobfuscate/åæ¥æ¯ snapshot 㯠core ã«éç´ã 5 // apiCall ã¯ä¸ã§ãã¼ã¸åºæã® onHttpErrorï¼ã»ãã·ã§ã³æéåã/硬å°ç¦ï¼ãæããèã©ããã«ããã 6 const { 7 AMAZON_TAG, CONFIG, STORE_COLORS, 8 KX_STORE_NAME_BY_KEY, KX_STORE_KEY_BY_NAME, 9 setXorKey, deobfuscatePrice, rawPrice, rawMaxPrice, 10 _isoMinusOne, _findYesterdaySnapshot 11 } = window.KX_CORE; 12 13 // ==================== DOM å ç´ ==================== 14 const elements = { 15 searchSection: document.getElementById('searchSection'), 16 productSection: document.getElementById('productSection'), 17 loading: document.getElementById('loading'), 18 searchInput: document.getElementById('searchInput'), 19 searchDropdown: document.getElementById('searchDropdown'), 20 searchResults: document.getElementById('searchResults'), 21 searchError: document.getElementById('searchError'), 22 backBtn: document.getElementById('backBtn'), 23 productName: document.getElementById('productName'), 24 productJan: document.getElementById('productJan'), 25 pricesGrid: document.getElementById('pricesGrid'), 26 statMaxPrice: document.getElementById('statMaxPrice'), 27 statStoreCount: document.getElementById('statStoreCount'), 28 statDiff: document.getElementById('statDiff'), 29 noHistory: document.getElementById('noHistory') 30 }; 31 32 let priceChart = null; 33 let currentJan = null; 34 let currentFrom = null; 35 let currentFromQuery = ''; // æ»ãå ã® query params ãä¿æ 36 let currentFromUrl = null; // æ»ãå ã®å ·ä½ç URLï¼listing ãã¼ã¸çï¼ 37 let _isInitialLoad = false; 38 let profitableData = null; 39 let carouselPage = 1; 40 let searchTimeout = null; 41 const CARDS_PER_PAGE = 5; 42 const SEARCH_DEBOUNCE_MS = 300; 43 const HISTORY_KEY = 'kaitorix_search_history'; 44 const MAX_HISTORY_ITEMS = 100; // ä¿åä¸éï¼è¡¨ç¤ºã¯åæ10ä»¶+ããã£ã¨è¦ããã§+10ãã¤æå¤§100ï¼ 45 const MATOME_KEY = 'kaitorix_matome_list'; 46 const MAX_MATOME_ITEMS = 50; 47 const FAVORITES_KEY = 'kaitorix_favorites'; 48 const MAX_FAVORITES = 50; 49 50 let matomeBarExpanded = false; 51 let previousViewState = null; 52 let currentProductData = null; 53 let currentHistoryData = null; 54 // å®ä¾¡å±¥æ´ [{from:'YYYY-MM-DD', msrp:int}, ...]ï¼effective_from æé ãæ«å°¾=ç¾è¡ï¼ 55 let currentMsrpHistory = null; 56 // çµ±è¨æ¬ã®åè¨ç®ã«ä½¿ããç¾å¨ã®ã¬ã³ã¸ãã¨ã表示ä¸åºè keyã 57 let _currentRecentHistory = null; 58 let _visibleStoreKeys = null; // null = å ¨åºèï¼åæååãã©ã¼ã«ããã¯ï¼ 59 60 // ==================== API è°ç¨ï¼core.apiCall ã«ãã¼ã¸åºæããã¯ãæããï¼ ==================== 61 // challenge èªå°ã»å ±éã¨ã©ã¼ã¯ kaitorix-core.js ãæ ãã主çã ããæã¤ 62 // ãDP session æéåãã¢ã¼ãã«ãã硬å°ç¦ â /blocked é·ç§»ãã onHttpError ã§æ³¨å ¥ããã 63 // å¤å®é ã¯å å®è£ ã¨åä¸: session â (challenge 㯠core ã¸å§è²) â blockedã 64 function apiCall(endpoint) { 65 return window.KX_CORE.apiCall(endpoint, function(status, data, response) { 66 // DP session æéåã (/history ã 403 ãè¿ããå ´å) â åèªè¾¼èªå°ã¢ã¼ãã« 67 if (status === 403 && data.detail === 'dp_session_required') { 68 _showSessionExpiredModal(); 69 throw new Error('session_expired'); 70 } 71 // challenge 㯠core å´ã«å¦çãããï¼false ãè¿ãã¦å§è²ï¼ã 72 if (status === 403 && 73 (data.challenge || response.headers.get('X-Challenge-Required'))) { 74 return false;
75 } 76 // ãã以å¤ã® 403ï¼ç¡¬å°ç¦ / rate banï¼â å°ç¦èª¬æãã¼ã¸ï¼é£çµ¡å ä»ãï¼ã¸é·ç§» 77 if (status === 403) { 78 window.location.href = '/blocked'; 79 throw new Error('blocked'); 80 } 81 return false; 82 }); 83 } 84 85 // ã¾ã¨ãï¼ä¸æ¬æ¯è¼ï¼ç¨ã®ä¸æ¬åå¾ã¯ kaitorix-core.js ã«éç´ï¼2026-09-21ï¼ã 86 // ããã«åããç½®ãã¦ããããã§ãTG çï¼kaitorix-tg.jsï¼ãåå¥å¼ã³åºãã®ã¾ã¾ 87 // åãæ®ãããåã誤 BAN ã 2026-09-21 ã«åçºãããå®è£ 㯠core å´ã® 88 // fetchProductsBatch åé ã³ã¡ã³ããåç §ãapiCall ã¯ä¸»çåºæããã¯ä»ããæ¸¡ãã 89 function fetchProductsBatch(jans) { 90 return window.KX_CORE.fetchProductsBatch(jans, apiCall); 91 } 92 93 // ã»ãã·ã§ã³æéåãã¢ã¼ãã«: ã¦ã¼ã¶ã¼ãæåã§åèªè¾¼ 94 let _sessionModalShown = false; 95 function _showSessionExpiredModal() { 96 if (_sessionModalShown) return; 97 _sessionModalShown = true; 98 const ov = document.createElement('div'); 99 ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px'; 100 ov.innerHTML = '<div style="background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:24px;max-width:400px;text-align:center;color:#eee"><div style="font-size:16px;font-weight:600;margin-bottom:12px">ã»ãã·ã§ã³æéåã</div><div style="font-size:14px;line-height:1.6;margin-bottom:20px;color:#bbb">ä¾¡æ ¼ãã£ã¼ãã®ã»ãã·ã§ã³ãæéåãã«ãªãã¾ããã<br>ãã¼ã¸ãåèªã¿è¾¼ã¿ãã¦ãã ããã</div><button id="_sessReloadBtn" style="background:#3b82f6;color:#fff;border:0;padding:10px 24px;border-radius:6px;font-size:14px;cursor:pointer">åèªã¿è¾¼ã¿</button></div>'; 101 document.body.appendChild(ov); 102 document.getElementById('_sessReloadBtn').onclick = () => location.reload(); 103 } 104 // setXorKey / deobfuscatePrice / _isoMinusOne / _findYesterdaySnapshot 㯠105 // kaitorix-core.js ã«éç´ï¼åé ã§åå²ä»£å ¥æ¸ã¿ï¼ã 106 107 // ==================== æç´¢åè½ ==================== 108 async function searchDropdown(query) { 109 query = query.trim(); 110 if (!query || query.length < 2) { 111 hideDropdown(); 112 return; 113 } 114 showDropdownLoading(); 115 116 // GA4: search 117 if (window.gtag) gtag('event', 'search', { search_term: query }); 118 119 try { 120 _dropdownQuery = query; 121 const data = await apiCall(`/api/search?q=${encodeURIComponent(query)}&limit=30&offset=0`); 122 if (!data.results || data.results.length === 0) { 123 showDropdownEmpty(query); 124 return; 125 } 126 renderDropdown(data.results); 127 } catch (err) { 128 hideDropdown(); 129 } 130 } 131 132 function showDropdownLoading() { 133 elements.searchDropdown.innerHTML = '<div class="dropdown-loading">æ¤ç´¢ä¸...</div>'; 134 elements.searchDropdown.classList.add('active'); 135 } 136 137 function showDropdownEmpty(query) { 138 var q = query ? escapeHtml(query) : ''; 139 elements.searchDropdown.innerHTML = ( 140 '<div style="padding:16px;text-align:center;color:var(--text-muted);">' + 141 '<div style="margin-bottom:8px;">ã' + q + 'ãã®æ¤ç´¢çµæã¯ããã¾ãã</div>' + 142 '<div style="font-size:0.85rem;">' + 143 '<p>åååã®ä¸é¨ã»JANã»ASINã§æ¤ç´¢ãã¦ã¿ã¦ãã ãã</p>' + 144 '<div style="margin-top:8px;">' + 145 '<a href="/guide" style="color:var(--primary);text-decoration:none;font-weight:600;">ä½¿ãæ¹ã¬ã¤ã</a>' + 146 '<span style="margin:0 8px;color:var(--border);">|</span>' + 147 '<a href="/ranking/profit" style="color:var(--primary);text-decoration:none;font-weight:600;">å©çã©ã³ãã³ã°ãè¦ã</a>' + 148 '</div></div></div>' + 149 _searchAllRow(query) 150 ); 151 elements.searchDropdown.classList.add('active'); 152 } 153 154 function hideDropdown() { 155 elements.searchDropdown.classList.remove('active'); 156 elements.searchDropdown.innerHTML = ''; 157 } 158 159 var _dropdownResults = []; 160 var _dropdownQuery = ''; 161 var _dropdownHasMore = true; 162 var _dropdownLoading = false;
163 var _DROPDOWN_PAGE = 30; 164 165 // æ¤ç´¢ãããããã¦ã³å ±éã®ããqãã§å ¨ä»¶æ¤ç´¢ãè¡ï¼ã¯ãªãã¯ã§ /search?q= ã¸é·ç§»ï¼ã 166 // å ¨ SPA æ¤ç´¢ããã¯ã¹ï¼hero / product inline / stickyï¼ã§åãå°ç·ãæä¾ããã 167 // isEnterAction: Enter ããã®è¡ã¨åãæåã«ãªãå ´åã®ã¿ keycap ãåºãã 168 // åè£ãã¡ããã© 1 ä»¶ã®ã¨ãã ã Enter ã¯ãã®ååã¸ç´è¡ããã®ã§ããã®å ´å㯠false ãæ¸¡ãï¼è¡¨ç¤ºã¨å®æåã®é½é½¬ãé²ãï¼ã 169 function _searchAllRow(q, isEnterAction) { 170 const qq = escapeHtml(q || ''); 171 const href = '/search?q=' + encodeURIComponent(q || ''); 172 const kbd = (isEnterAction === false) ? '' : '<kbd class="search-all-kbd">Enter âµ</kbd>'; 173 return `<a href="${href}" class="search-all-row" aria-label="ã${qq}ãã§å ¨ä»¶æ¤ç´¢">` 174 + `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.3" y2="16.3"/></svg>` 175 + `<span class="sar-label">ã<b>${qq}</b>ãã§å ¨ä»¶æ¤ç´¢</span>${kbd}</a>`; 176 } 177 178 function _renderDropdownBody() { 179 let html = _dropdownResults.map(item => { 180 const img = item.image_url 181 ? `<img src="${escapeHtml(item.image_url)}" loading="lazy" alt="" style="width:40px;height:40px;object-fit:contain;border-radius:4px;background:#fff;flex-shrink:0;">` 182 : `<div style="width:40px;height:40px;border-radius:4px;background:var(--border);flex-shrink:0;"></div>`; 183 return ` 184 <a href="/dp/${escapeHtml(item.jan)}" class="dropdown-item" data-jan="${escapeHtml(item.jan)}" style="display:flex;align-items:center;gap:10px;"> 185 ${img} 186 <div style="flex:1;min-width:0;"> 187 <div class="dropdown-item-name">${escapeHtml(item.name)}</div> 188 <div class="dropdown-item-code">JAN: ${escapeHtml(item.jan)}</div> 189 </div> 190 </a>`; 191 }).join(''); 192 if (_dropdownHasMore) { 193 const label = _dropdownLoading ? 'èªã¿è¾¼ã¿ä¸...' : `ããã«èªã¿è¾¼ã (+${_DROPDOWN_PAGE})`; 194 html += `<div id="dropdownLoadMore" style="padding:12px;text-align:center;cursor:pointer;color:var(--primary);font-weight:600;font-size:0.88rem;border-top:1px solid var(--border);">${label}</div>`; 195 } 196 html += _searchAllRow(_dropdownQuery, _dropdownResults.length !== 1); 197 elements.searchDropdown.innerHTML = html; 198 elements.searchDropdown.classList.add('active'); 199 200 elements.searchDropdown.querySelectorAll('.dropdown-item').forEach(el => { 201 el.addEventListener('click', (e) => { 202 e.preventDefault(); 203 hideDropdown(); 204 currentFrom = 'search'; 205 _updateBackBtn('search'); 206 showProduct(el.dataset.jan); 207 }); 208 }); 209 const moreBtn = document.getElementById('dropdownLoadMore'); 210 if (moreBtn) { 211 moreBtn.addEventListener('click', async (e) => { 212 e.stopPropagation(); 213 if (_dropdownLoading || !_dropdownHasMore) return; 214 _dropdownLoading = true; 215 _renderDropdownBody(); 216 try { 217 const offset = _dropdownResults.length; 218 const data = await apiCall(`/api/search?q=${encodeURIComponent(_dropdownQuery)}&limit=${_DROPDOWN_PAGE}&offset=${offset}`); 219 const more = (data && data.results) || []; 220 _dropdownResults = _dropdownResults.concat(more); 221 _dropdownHasMore = more.length >= _DROPDOWN_PAGE; 222 } catch (err) { 223 _dropdownHasMore = false;
224 } 225 _dropdownLoading = false; 226 _renderDropdownBody(); 227 }); 228 } 229 } 230 231 function renderDropdown(results) { 232 _dropdownResults = results || []; 233 _dropdownHasMore = _dropdownResults.length >= _DROPDOWN_PAGE; 234 _dropdownLoading = false; 235 _renderDropdownBody(); 236 } 237 238 function debouncedSearch() { 239 if (searchTimeout) clearTimeout(searchTimeout); 240 searchTimeout = setTimeout(() => { 241 searchDropdown(elements.searchInput.value); 242 }, SEARCH_DEBOUNCE_MS); 243 } 244 245 // å ¥åæ¬å ã®ãEnter âµããã³ãã®åºãå ¥ããEnter ãå®éã«å¹ãé¾å¤ï¼2æåï¼ã«åãããã 246 // 表示èªä½ã¯ PC éå®ï¼CSS ã® pointer:fine ã¡ãã£ã¢ã¯ã¨ãªå´ã§å¶å¾¡ï¼ã 247 function syncEnterHint() { 248 const box = elements.searchInput && elements.searchInput.closest('.search-box'); 249 if (!box) return; 250 box.classList.toggle('has-query', (elements.searchInput.value || '').trim().length >= 2); 251 } 252 253 // ==================== Page View Tracking ==================== 254 let _lastTrackedPath = null; 255 function trackPageView(path) { 256 path = path || location.pathname; 257 if (path === _lastTrackedPath) return; 258 // ååãããã®ã¿ document.referrer ãéãï¼SEO/GEO æµå ¥å è¨æ¸¬ãSPA å é·ç§»ã¯ ref ãªãï¼ 259 const _ref = _lastTrackedPath === null ? (document.referrer || '') : ''; 260 _lastTrackedPath = path; 261 fetch('/api/pageview?token=' + CONFIG.API_TOKEN, { 262 method: 'POST', 263 headers: {'Content-Type': 'application/json', 'X-API-Token': CONFIG.API_TOKEN}, 264 body: JSON.stringify({path: path, ref: _ref}) 265 }).catch(() => {}); 266 } 267 268 // ==================== åå详æ ==================== 269 // ååéé·ç§»æï¼é¢é£ååã¯ãªãã¯çï¼: å¤ãä¾¡æ ¼è¡¨ãæ®ãã skeleton ãå³è¡¨ç¤º 270 function renderPriceSkeleton(rows) { 271 if (!elements.pricesGrid) return; 272 var html = '<div class="prices-list" aria-busy="true" aria-label="ä¾¡æ ¼ãèªã¿è¾¼ã¿ä¸">'; 273 for (var i = 0; i < (rows || 6); i++) { 274 html += '<div class="price-skeleton-row">' + 275 '<div style="display:flex;align-items:center;flex:1;min-width:0;">
275' + 276 '<span class="sk-line sk-rank"></span><span class="sk-line sk-store"></span></div>' + 277 '<span class="sk-line sk-price"></span></div>'; 278 } 279 elements.pricesGrid.innerHTML = html + '</div>'; 280 } 281 282 // ãããçããååã¸ç§»ãæã¯ãã¾ããååãèªã¿è¾¼ã¿ä¸ãã®å½¢ï¼price-checker.html ã® kx-dp-boot 㨠283 // åãï¼ã«ãã¦ããã®ä½ç½®ã¸å³åº§ã«éãã以åã¯èªã¿è¾¼ã¿ä¸ããããã®ç»é¢ã®ã¾ã¾ï¼èªã¿è¾¼ã¿è¡¨ç¤ºã¯ 284 // æ¤ç´¢æ¬ã®ä¸ã§ç»é¢å¤ã®ãã¨ãï¼ãå±ããç¬éã«ä¸èº«ãå ¥ãæ¿ãã£ã¦ããç»é¢ãæµãã¦ãã 285 // ï¼2026-10-01 Mayãå ã«å´©ããå½¢ãåºã¦ãä¸ç¬ã§æ£å¼ã«å¤ãããï¼ã 286 // éãå ã¯ååãæãããä½ç½®ï¼showProduct ã®æå¾ã®ã¹ã¯ãã¼ã«ã¨åãå¼ã 287 function _enterProductLoadingView() { 288 document.documentElement.classList.add('kx-dp-boot'); 289 const tb = document.querySelector('.sticky-toolbar'); 290 const off = tb ? tb.offsetHeight + 8 : 0; 291 const top = elements.loading.getBoundingClientRect().top + window.pageYOffset - off; 292 window.scrollTo({ top: Math.max(0, top), behavior: 'auto' }); 293 } 294 295 async function showProduct(jan, scrollToTop) { 296 showLoading(true); 297 hideError(); 298 currentJan = jan; 299 if (elements.productSection.classList.contains('active')) renderPriceSkeleton(6); 300 else if (scrollToTop || currentFrom) _enterProductLoadingView(); // æå¾ã«ååã¸ã¹ã¯ãã¼ã«ããçµè·¯ã ã 301 302 try { 303 // /api/product/{jan} ãå ã« await ã㦠dp_seen marker ã確å®ã« Redis ã« 304 // æ¸ãè¾¼ã¾ããããã®å¾ /history ãå¼ã¶ãã¨ã§ marker ãã§ãã¯ãéãã 305 // 並åã«ãã㨠race condition ã§ /history ãå ã«å°çã 403 ã«ãªãã 306 const product = await apiCall(`/api/product/${jan}?campaign=1`); 307 _productReadyJan = jan; // ããã§åã㦠dp_seen marker ãç«ã£ã¦ãã 308 const historyData = await apiCall(`/api/product/${jan}/history?days=180`); 309 let peakData = null; 310 try { peakData = await apiCall(`/api/product/${jan}/peak`); } catch (e) { peakData = null; } 311 312 showLoading(false); 313 currentHistoryData = historyData.history; 314 currentMsrpHistory = (historyData.msrp_history && historyData.msrp_history.length) ? historyData.msrp_history : null; 315 setXorKey(product._k); 316 renderProduct(product); 317 renderPeakPrice(peakData && peakData.peak); 318 // 表示æé: è¨æ¶ãããè¨å® or PC=30æ¥/ã¢ãã¤ã«=14æ¥ 319 const _initDays = _chartPeriodPref(); 320 const _rangeSel = document.getElementById('chartRangeSel'); 321 if (_rangeSel) _rangeSel.value = String(_initDays); 322 // ð´ð´ ãªã»ãã㯠**renderChart ããå**ãé çªãéã«ãã㨠2 ã¤å£ãã: 323 // ã»`_plogHidden` 㯠`renderChart` ã®æå¾ã§å¡ä¾ã®ç¶æ ããä½ãç´ãããã 324 // å¾ãã空ã«ããã¨ãã°ã©ããé ãã¦ããåºã表ã§ã¯åºãã¾ã¾ã«ãªãã 325 // å ã«ç©ºã«ãã¦ããã°ãæ£å¸¸ç³»ï¼renderChart ãæ£ããå ¥ãç´ãã 326 // ãç°å¸¸ç³»ï¼renderChart ãæ©æ returnï¼å±¥æ´ 0 ä»¶ / Chart æªãã¼ãï¼ 327 // ãã¦ã空ã®ã¾ã¾ï¼åæã«çµãè¾¼ã¾ããªããã®ä¸¡æ¹ãæãç«ã¤ã 328 // ã»`plogPrev`ï¼å±¥æ´ãã¥ã¼ã§é ããæã®å ã® displayï¼ãåããå ã«æ¶ãã° 329 // `renderChart` ããã®ååç¨ã«åºãç´ãã #chartMsrpToggle / 330 // #chartBrush / #chartPeriodStats ãã徿®µã® `setPriceLogView(false)` 331 // ã**åã®ååã®å¤**ã§ä¸æ¸ãããªãï¼å®ä¾¡ã®ç¡ãååâæãååã¸ç§»ãã¨
332 // å®ä¾¡ãã°ã«ãåºãªãã¾ã¾ãã¨ããå£ãæ¹ããã¦ããï¼ã 333 _plogLoadedJan = null; 334 _plogAll = []; 335 _plogPage = 1; 336 _plogHidden = new Set(); 337 // ð´ð´ å°ãæ¶ãåã« **å¿ ãæ»ã**ã`renderChart` ã display ãå ¥ãç´ãã®ã¯ 338 // noHistory / chartBrush / chartPeriodStats / chartMsrpToggle ã® 4 ã¤ã ãã§ã 339 // **chartSaleBtn / chartRangeSel / chartSaleNote ã¯èª°ã触ããªã**ã 340 // å°ã ãæ¶ãã¨å±¥æ´ãã¥ã¼ã§ä»ãã inline ã® display:none ãæ®ãã 341 // ã»ã¼ã«ã¨æéã»ã¬ã¯ã¿ã**以å¾ã©ã®ååã§ãåºãªããªã**ï¼ï¼ã°ã©ãã® 342 // æéãå¤ããææ®µã丸ãã¨æ¶ãããã¼ã¸ãèªã¿ç´ãã¾ã§æ»ããªãï¼ã 343 // ååãé åºãåã«åºããã ãã§ç´ããã¤ããã«ãªã£ã¦ããç©´ã 344 ['chartSaleBtn', 'chartRangeSel', 'chartSaleNote', 'chartBrush', 345 'chartPeriodStats', 'chartMsrpToggle', 'noHistory'].forEach(function (id) { 346 var el = document.getElementById(id); 347 if (!el || el.dataset.plogPrev === undefined) return; 348 el.style.display = el.dataset.plogPrev; 349 delete el.dataset.plogPrev; 350 }); 351 renderChart(currentHistoryData, _initDays); 352 setupPriceLogTabs(); 353 setupPriceLogPager(); 354 setPriceLogView(false); 355 renderPriceIndicator(historyData.price_indicator); 356 357 if (product.name && product.jan) { 358 saveToHistory(product.jan, product.name, { 359 // ð´ ç´ ã®æé«å¤ããã£ã³ãã¼ã³ä¾¡ãå ¥ãã㨠localStorage 㨠360 // /api/me/recent-views ã«æ¡ä»¶æãã®é¡ã**æéçµäºå¾ãæ®ã** 361 // ï¼å±¥æ´ã«ã¼ãã¯æ¡ä»¶ãæããªãããæéã§æ¶ããä»çµã¿ãç¡ãï¼ã 362 price: rawMaxPrice(product.prices) || deobfuscatePrice(product.max_price) || 0, 363 img: product.image_url || '', 364 cat: product.category || '' 365 }); 366 } 367 loadSokuho(jan); 368 369 // GA4: view_item 370 if (window.gtag) gtag('event', 'view_item', { 371 item_id: jan, 372 item_name: product.name, 373 price: rawMaxPrice(product.prices) || deobfuscatePrice(product.max_price) 374 }); 375 376 // Dynamic meta update for product page 377 // ð´ meta/og ã¯ãªã³ã¯ãã¬ãã¥ã¼ã¨ãã¦æ¡ä»¶ããåãé¢ããã¦åºåãã®ã§ç´ ã®é¡ã§æ¸ãã 378 const _prices = product.prices || []; 379 const _rawMax = rawMaxPrice(_prices); 380 const _maxPrice = _rawMax.toLocaleString(); 381 const _maxStore = (_prices.find(function (x) { return !x.stale && rawPrice(x) === _rawMax; }) 382 || _prices[0] || {}).store || ''; 383 const _name = product.name || ''; 384 // ð´ 2026-09-23ã è²·å価ã 1 åãç¡ãååãã«ã¿ãã°ã«è¼ãï¼JANâASIN ã® 385 // 対å¿ãæãã°æ²è¼ãã May è£æ±ºï¼ãç´ ã®ã¾ã¾ã ã¨ã0åã/ æé«å¤ã¯ ã®0å 386 // ã¨ããåã titleã»ogã»twitter ã«åæã«åºããSSR å´ï¼rendering.py ã® 387 // has_priceï¼ã¨åãåå²ããã¡ãã«ãç½®ãã 388 const _hasPrice = _rawMax > 0; 389 const _metaTitle = _hasPrice 390 ? `ã${_name}ãã®è²·åä¾¡æ ¼æ¯è¼ã${_maxPrice}åã- è²·åX` 391 : `ã${_name}ãã®è²·åä¾¡æ ¼ - è²·åX | ç¾å¨ãè²·åä¾¡æ ¼ã®æ²è¼ã¯ããã¾ãã`; 392 const _metaDesc = _hasPrice 393 ? `ã${_name}ãã®è²·åä¾¡æ ¼ãè¤æ°ç¤¾ã§æ¯è¼ãæé«å¤ã¯${_maxStore}ã®${_maxPrice}åãææ°ã®è²·åä¾¡æ ¼ãæ¯æ¥æ´æ°ä¸ã` 394 : `ã${_name}ãã¯ç¾å¨ãè²·åä¾¡æ ¼ã®æ²è¼ãããã¾ãããè²·åä¾¡æ ¼ãä»ã次第ãã®ãã¼ã¸ã«æ²è¼ãã¾ãã`; 395 const _canonical = `https://kaitorix.app/dp/${product.jan}
395`; 396 document.title = _metaTitle; 397 const _descEl = document.querySelector('meta[name="description"]'); 398 if (_descEl) _descEl.setAttribute('content', _metaDesc); 399 const _ogTitle = document.querySelector('meta[property="og:title"]'); 400 if (_ogTitle) _ogTitle.setAttribute('content', _metaTitle); 401 const _ogDesc = document.querySelector('meta[property="og:description"]'); 402 if (_ogDesc) _ogDesc.setAttribute('content', _metaDesc); 403 let _canonEl = document.querySelector('link[rel="canonical"]'); 404 if (!_canonEl) { _canonEl = document.createElement('link'); _canonEl.rel = 'canonical'; document.head.appendChild(_canonEl); } 405 _canonEl.setAttribute('href', _canonical); 406 407 elements.searchSection.style.display = 'none'; 408 elements.productSection.classList.add('active'); 409 // ç´éãã®ãèªã¿è¾¼ã¿ä¸ã®å½¢ãï¼price-checker.html ã® kx-dp-bootï¼ã¯ããã§çµãã 410 document.documentElement.classList.remove('kx-dp-boot'); 411 // ð´ ããã§åã㦠#productSection ã display:none ãè±ããrenderChart 㯠412 // ãã®**å**ã«èµ°ãã®ã§ã`_drawBrushMini` ã¯è¦ªå¹ 0 ãè¦ã¦ï¼0 å¹ ã¬ã¼ãã§ï¼ 413 // æããã«å¸°ã£ã¦ããï¼ URL ç´éãã»æ¤ç´¢âååã®ååã§ãããããã 414 // 空ã®ã¾ã¾æ®ããæ§å®è£ 㯠`|| 300` ã§ã¼ããã¤ã¤ãæãã¦ããã®ã§ã 415 // ã¬ã¼ãã ãè¶³ããã®ã¯**çæè½ã¡**ã ã£ããè¦ããç´å¾ã«æãç´ãã 416 if (currentHistoryData && currentHistoryData.length) _drawBrushMini(currentHistoryData); 417 updateMatomeUI(); 418 // from_url ã URL ã«æ®ãï¼ãªãã¼ã/popstate å¾ããâ¯â¯ã«æ»ããã®æ»ãå ãç¶æï¼ 419 var _navParams = new URLSearchParams(); 420 if (currentFrom) { 421 _navParams.set('from', currentFrom); 422 if (currentFromUrl) _navParams.set('from_url', currentFromUrl); 423 } 424 var _navQs = _navParams.toString(); 425 var productUrl = '/dp/' + jan + (_navQs ? '?' + _navQs : ''); 426 const _firstView = _isInitialLoad; 427 if (_isInitialLoad) { 428 history.replaceState({ jan: jan }, '', productUrl); 429 _isInitialLoad = false; 430 } else { 431 history.pushState({ jan: jan }, '', productUrl); 432 } 433 trackPageView('/dp/' + jan); 434 435 // Scroll after rendering - when navigating from any section with from parameter 436 if (scrollToTop || currentFrom) { 437 requestAnimationFrame(function() { 438 const el = document.getElementById('productSection'); 439 const headerH = document.querySelector('.sticky-toolbar'); 440 const offset = headerH ? headerH.offsetHeight + 8 : 0; 441 const top = el.getBoundingClientRect().top + window.pageYOffset - offset; 442 // ç´éãã¯èªã¿è¾¼ã¿ä¸ã®æç¹ã§ããåãä½ç½®ã¸éã£ã¦ããï¼price-checker.htmlï¼ã 443 // ããã§ smooth ã«ããã¨ããããæ° px ã§ãç»é¢ãæµãã¦è¦ããã®ã§å³æã«åãããã 444 window.scrollTo({top: top, behavior: _firstView ? 'auto' : 'smooth'}); 445 }); 446 } 447 448 } catch (err) { 449 // 404 = DB ã«åå¨ããªã JANï¼Open API çããä»»æ JAN ã§çå°ãå¾ãï¼ã 450 // çã®ãHTTP 404ãã§ã¯ãªãæ¡å æãåºã 451 if (String(err.message).indexOf('404') >= 0) { 452 showError('ãã®ååã¯ç¾å¨ãåãæ±ããããã¾ããï¼è²·ååºã§ã®åæ±ã確èªã§ããªãååã§ãï¼ãåååã»åçªã»JANã³ã¼ãã§æ¤ç´¢ãã試ããã ããã'); 453 } else { 454 showError(err.message); 455 } 456 showLoading(false); 457 // æ¡å æã¯æ¤ç´¢æ¬ã®ä¸ã«åºãï¼èªã¿è¾¼ã¿ä¸ã®å½¢ãè§£ãã¦æ¤ç´¢æ¬ãè¦ãã 458 document.documentElement.classList.remove('kx-dp-boot'); 459 } 460 } 461 462 function renderProduct(product) { 463 currentProductData = { 464 jan: product.jan, 465 name: product.name, 466 image_url: product.image_url || null, 467 prices: product.prices || [], 468 asin: product.asin || null 469 }; 470
471 updateFavoriteStarUI(product.jan); 472 473 document.getElementById('qtyInput').value = 1; 474 document.getElementById('matomeAddFeedback').textContent = ''; 475 document.getElementById('matomeAddFeedback').className = ''; 476 // Reset profit calculator & PC qty 477 const profitCalcInput = document.getElementById('profitCalcInput'); 478 if (profitCalcInput) { profitCalcInput.value = ''; } 479 const profitCalcResult = document.getElementById('profitCalcResult'); 480 if (profitCalcResult) { profitCalcResult.textContent = 'å©ç Â¥---'; profitCalcResult.className = 'profit-calc-result zero'; } 481 const qtyInputPcEl = document.querySelector('.qty-input-pc'); 482 if (qtyInputPcEl) { qtyInputPcEl.value = 1; } 483 484 elements.productName.textContent = product.name || 'ååå䏿'; 485 elements.productJan.textContent = product.jan; 486 elements.productJan.dataset.jan = product.jan; 487 488 const imageContainer = document.getElementById('productImage'); 489 if (product.image_url) { 490 imageContainer.innerHTML = `<img src="${escapeHtml(product.image_url)}" alt="${escapeHtml(product.name || '')}" loading="lazy" width="200" height="200">`; 491 } else { 492 imageContainer.innerHTML = '<div class="placeholder-icon" style="width:100%;height:100%;border-radius:12px;">'+placeholderSVG(64)+'</div>'; 493 } 494 imageContainer.style.display = 'flex'; 495 496 const prices = product.prices || []; 497 const maxPrice = prices.length > 0 ? deobfuscatePrice(prices[0].price) : 0; 498 const minPrice = prices.length > 0 ? deobfuscatePrice(prices[prices.length - 1].price) : 0; 499 500 // ð´ å¤ãç¡ãæã« Â¥0 ã¨åºãã¨ã0 åã§ããè²·ãåãããªããã¨èªãããSSR 㯠"---"ã 501 elements.statMaxPrice.textContent = maxPrice > 0 ? `Â¥${maxPrice.toLocaleString()}` : '---'; 502 // å®ä¾¡å¯¾æ¯ã¹ããªããï¼ããååã®ã¿: Apple/主è¦ã²ã¼ã æ©ï¼: å®ä¾¡ â æ¯è¼ãã¼ â è²·åç 503 const _msrpEl = document.getElementById('statMsrp'); 504 if (_msrpEl) { 505 const _msrpNow = product.msrp || 0; 506 if (_msrpNow > 0 && maxPrice > 0) { 507 const _rate = Math.round(maxPrice / _msrpNow * 1000) / 10; 508 _msrpEl.innerHTML = 509 `<span>å®ä¾¡ Â¥${_msrpNow.toLocaleString()}</span>` 510 + `<span class="stat-msrp-bar"><i style="width:${Math.min(_rate, 100)}%"></i></span>` 511 + `<span>è²·åç <b>${_rate}%</b></span>`; 512 _msrpEl.title = `æé«è²·åä¾¡æ ¼ã¯å®ä¾¡ï¼ç¨è¾¼ï¼ã® ${_rate}%`; 513 _msrpEl.style.display = ''; 514 } else { 515 _msrpEl.style.display = 'none'; 516 } 517 } 518 syncStatsCtx(); 519 elements.statStoreCount.textContent = `${prices.length}åºè`; 520 // 忥æ¯ï¼ç»é¢ã«è¡¨ç¤ºä¸ã®æé«å¤ vs æ¨æ¥ snapshotï¼ 521 // â» history API ã®ã仿¥è¡ãã§ã¯ãªãè¡¨ç¤ºä¾¡æ ¼ãã®ãã®ã使ã â 522 // product 㨠history ã®ãã£ãã·ã¥æéãºã¬ã§ãä¾¡æ ¼ã¯æ§ã»ããã¸ã¯æ°ãã® 523 // 䏿´å表示ãèµ·ããããï¼2026-06-12 ãã ã©å¼·åä¾¡æ ¼äºæ¡ï¼ 524 const _yesterdayRow = _findYesterdaySnapshot(currentHistoryData); 525 if (_yesterdayRow && maxPrice > 0) { 526 const yPrices = Object.values(_yesterdayRow.prices || {}).filter(v => v > 0); 527 if (yPrices.length) { 528 const yesterdayMax = Math.max(...yPrices); 529 const diff = maxPrice - yesterdayMax; 530 if (diff > 0) { 531 elements.statDiff.innerHTML = `<span style="color:var(--success)">+Â¥${diff.toLocaleString()}</span>`; 532 } else if (diff < 0) { 533 elements.statDiff.innerHTML = `<span style="color:var(--danger)">
533-Â¥${Math.abs(diff).toLocaleString()}</span>`; 534 } else { 535 elements.statDiff.textContent = '±¥0'; 536 } 537 } else { 538 elements.statDiff.textContent = '---'; 539 } 540 } else { 541 elements.statDiff.textContent = '---'; 542 } 543 544 const ecContainer = document.getElementById('amazonBtnContainer'); 545 const productName = encodeURIComponent(product.name); 546 547 renderCheckerBanner(product.jan); 548 549 const amazonUrl = product.asin 550 ? `https://www.amazon.co.jp/dp/${escapeHtml(product.asin)}?tag=${AMAZON_TAG}&linkCode=ogi&th=1&psc=1` 551 : `https://www.amazon.co.jp/s?k=${productName}`; 552 // jan ãæ¸¡ã: ä¸è»¢ãã¼ã¸ãèªç¤¾ DB ããåååã»åçãå¼ãã¦ååãã¼ã¸ä½è£ã§åºãã 553 // æ¤ç´¢èªä½ã JAN å®å ¨ä¸è´ã«ãªãï¼åååæ¤ç´¢ã¯å®¹éã»è²éããæ¾ãï¼ 554 const _janQ = encodeURIComponent(product.jan || ''); 555 const rakutenUrl = window.kxRakutenUrl({ jan: product.jan || '', query: product.name || '' }); 556 const yahooUrl = `https://mottoku.app/y/search?jan=${_janQ}&q=${productName}&s=ktrx`; 557 558 const fujiyaUrl = `https://www.fujiya-camera.co.jp/shop/goods/search.aspx?keyword=${product.jan || productName}`; 559 const yodobashiUrl = `https://www.yodobashi.com/?word=${productName}`; 560 const sofmapUrl = product.jan 561 ? `https://click.linksynergy.com/link?id=IQNT7UmwfIQ&offerid=260508.37641101644937&type=2&murl=${encodeURIComponent('https://www.sofmap.com/search_result.aspx?keyword=' + product.jan)}` 562 : `https://www.sofmap.com/`; 563 564 const _jan = product.jan || ''; 565 ecContainer.innerHTML = ` 566 <div class="ec-buttons" style="display:flex;gap:8px;align-items:center;"> 567 <a href="${amazonUrl}" target="_blank" rel="sponsored noopener" class="ec-btn amazon" style="flex:1;">Amazon</a> 568 <a href="${rakutenUrl}" target="_blank" rel="sponsored noopener" class="ec-btn rakuten" style="flex:1;" onclick="return showShopsPopover(this,'${_jan}','rakuten','${escapeHtml(rakutenUrl)}',event)">楽天</a> 569 <a href="${yahooUrl}" target="_blank" rel="sponsored noopener" class="ec-btn yahoo" style="flex:1;" onclick="return showShopsPopover(this,'${_jan}','yahoo','${escapeHtml(yahooUrl)}',event)">Yahoo</a> 570 <span class="more-shops-btn" style="display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:8px;background:#f1f5f9;cursor:pointer;transition:all .15s;flex-shrink:0;" title="ä»ã®ã·ã§ããã§æ¤ç´¢" onclick="toggleMoreShops(this,'${productName}','${escapeHtml(amazonUrl)}','${escapeHtml(rakutenUrl)}','${escapeHtml(yahooUrl)}','${escapeHtml(fujiyaUrl)}','${escapeHtml(yodobashiUrl)}','${escapeHtml(sofmapUrl)}','${_jan}')"> 571 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.3" y2="16.3"/><line x1="8" y1="11" x2="14" y2="11"/></svg> 572 </span> 573 </div> 574 `; 575 576 // async: update button labels with shop counts AND populate prefetch cache 577 // for synchronous click handling in showShopsPopover (mobile compatibility) 578 if (_jan) { 579 if (!window._shopsCache) window._shopsCache = {}; 580 const _token = (typeof CONFIG !== 'undefined' && CONFIG.API_TOKEN) ? CONFIG.API_TOKEN : ''; 581 ['rakuten', 'yahoo'].forEach(function(src) { 582 const _key = _jan + ':' + src; 583 if (window._shopsCache[_key] !== undefined) return; 584 window._shopsCache[_key] = null; // pending 585 fetch('/api/shops/' + _jan + '/' + src + '?token=' + _token, {headers: {'X-API-Token': _token}}) 586 .then(function(r) { return r.ok ? r.json() : {shops:[]}; }) 587 .then(function(data) { 588 const shops = data.shops || []; 589 window._shopsCache[_key] = shops; 590 if (!shops.length) return; 591 const btn = ecContainer.querySelector('.ec-btn.' + src); 592 if (btn) btn.textContent = (src === 'rakuten' ? '楽天' : 'Yahoo') + ' ' + shops.length; 593 }).catch(function() { window._shopsCache[_key] = []; }); 594 }); 595 } 596 597 // ååºèã®åæ¥æ¯ï¼ç»é¢ã«è¡¨ç¤ºä¸ã®ååºä¾¡æ ¼ vs æ¨æ¥ snapshot â ä¸ã®æé«å¤åæ¥æ¯ã¨åæ¹éï¼ 598 const storeDiffMap = {}; 599 const nameToKey = KX_STORE_KEY_BY_NAME; 600 const _yesterdayStoreRow = _findYesterdaySnapshot(currentHistoryData); 601 if (_yesterdayStoreRow) {
602 prices.forEach(p => { 603 const key = p.key || nameToKey[p.store]; 604 if (!key) return; 605 const tp = deobfuscatePrice(p.price) || 0; 606 const yp = (_yesterdayStoreRow.prices || {})[key] || 0; 607 if (tp > 0 && yp > 0) storeDiffMap[p.store] = tp - yp; 608 }); 609 } 610 611 const firstActiveIdx = prices.findIndex(p => !p.stale); 612 const _msrp = product.msrp || 0; 613 // %ã®æå³ã¯è¡ãã¨ã«æ¸ãã¨åé·ãªã®ã§ããªã¹ãä¸é¨ã«1åã ãæ³¨è¨ãã 614 const _rateCaption = _msrp > 0 615 ? `<div class="rate-caption">%ã¯å®ä¾¡ï¼Â¥${_msrp.toLocaleString()}ï¼ã«å¯¾ããè²·åç</div>` : ''; 616 elements.pricesGrid.innerHTML = `${_rateCaption}<div class="prices-list${_msrp > 0 ? ' rated' : ''}">${prices.map((p, i) => { 617 const diff = storeDiffMap[p.store]; 618 const _arrowUp = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>'; 619 const _arrowDown = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>'; 620 const diffHtml = diff > 0 ? `<span class="store-diff up" title="åæ¥æ¯ +${diff.toLocaleString()}å">${_arrowUp}${diff.toLocaleString()}</span>` 621 : diff < 0 ? `<span class="store-diff down" title="åæ¥æ¯ -${Math.abs(diff).toLocaleString()}å">${_arrowDown}${Math.abs(diff).toLocaleString()}</span>` 622 : `<span class="store-diff none">---</span>`; 623 const _storeKey = p.key || nameToKey[p.store] || p.store; 624 const _janCode = (typeof product !== 'undefined' && product.jan) ? product.jan : ''; 625 const _pname = (typeof product !== 'undefined' && product.name) ? product.name.replace(/'/g, '') : ''; 626 const isHighest = i === firstActiveIdx; 627 // åºè伿¥ï¼ãçä¼ã¿çï¼: ãµã¼ãã¼ãã仿¥ãæéå ã®åºãã«ã ã holiday ãä»ããã 628 // æéãéããã°ãµã¼ãã¼å´ã§æ¶ããã®ã§ããã³ãã«è§£é¤å¦çã¯è¦ããªãã 629 const _hol = (p.holiday && p.holiday.text) ? p.holiday : null; 630 // æééå®ãã£ã³ãã¼ã³ï¼ç¹å¥è²·åï¼: åãããµã¼ãã¼ããä»ãæéå ã®åºãã«ã ã 631 // campaign ãä»ããï¼data/store_campaigns.jsonï¼ãurl ãããã°æ¡ä»¶ã®è©³ç´°ã¸ã®ãªã³ã¯ã 632 // å¤é¨ãªã³ã¯ãªã®ã§ iOS ã® Universal Link ææ¸ï¼/js/exit-link.jsï¼ãèªåã§æããã 633 const _camp = (p.campaign && p.campaign.text) ? p.campaign : null; 634 const _nameInner = `<a href="${escapeHtml(p.url)}" target="_blank" ${kxStoreLinkAttrs(p.url)} onclick="if(typeof gtag==='function')gtag('event','affiliate_click',{store:'${_storeKey}',jan:'${_janCode}',product_name:'${_pname}'});">${escapeHtml(p.store)}</a>${isHighest ? '<span class="highest-badge"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M2 7l4.5 4L12 4l5.5 7L22 7l-2 13H4L2 7z"/></svg>æé«å¤</span>' : ''}${p.stopped ? '<span class="stopped-badge" title="åºèå´ã§ãç³è¾¼ã¿ã忢ä¸ã§ãï¼éé¡ã¯åºèã®æ²ç¤ºé¡ï¼">ç³è¾¼åæ¢ä¸</span>' : ''}`; 635 // å°åãä»ãè¡ã ããåºå+ããã¸ãã1è¡ã«å ãã§ç¸¦ç©ã¿ã«ããï¼.store-name ã column åï¼ã 636 // flex-wrap ã§æ¸ã¾ããã¨çå¹ ã§ãæé«å¤ãããã¸ãåç¬ã§1è¡è½ã¡ãã¦è¡ã伸ã³ãããã 637 const _holHtml = _hol 638 ? `<span class="store-subnote store-holiday" title="${escapeHtml(_hol.note || _hol.text)}">${escapeHtml(_hol.text)}</span>` : ''; 639 // ãã£ã³ãã¼ã³ã¯ãå°ãåã«ç·ã®ãããã§ç½®ãï¼May è£æ±º 2026-09-05ï¼ãåºåã®ä¸ã« 640 // ç©ãã¨åºåã¾ã¼ã³ã 200px åºå®ãªã®ã§ 3 è¡ã«æãã¦è¡ã ããçªåºããå³ã®å°å㯠641 // 空ããã¾ã¾ã ã£ãã形㯠.store-mark ã¨åãï¼ãã®åºã«ä»ãæ¡ä»¶ãã¨æå³ãæãã 642 const _campHtml = (() => { 643 if (!_camp) return ''; 644 const tip = escapeHtml(_camp.note || _camp.text); 645 const cta = (_camp.url && _camp.link_text) 646 ? `<span class="store-campaign-cta">${escapeHtml(_camp.link_text)}</span>` : ''; 647 const inner = `<span class="store-campaign-txt">${escapeHtml(_camp.text)}</span>${cta}`; 648 return _camp.url 649 ? `<a class="store-campaign" href="${escapeHtml(_camp.url)}" target="_blank" rel="noopener" title="${tip}">${inner}</a>` 650 : `<span class="store-campaign" title="${tip}">${inner}</span>`;
651 })(); 652 const _hasSub = !!_holHtml; // åºåã®ä¸ã«ç©ãã®ã¯ä¼æ¥ã ãã«ãªã£ã 653 const _nameHtml = _hasSub 654 ? `<span class="store-name-row">${_nameInner}</span><span class="store-subnotes">${_holHtml}</span>` 655 : _nameInner; 656 const classes = ['price-row']; 657 if (isHighest) classes.push('highest'); 658 if (p.stale) classes.push('stale'); 659 if (_hasSub) classes.push('has-subnote'); // çå¹ ã§å°åã®è¡ã ã 3 è¡ã°ãªããã«çµã¿æ¿ãã 660 if (_campHtml) classes.push('has-campaign'); // çå¹ ã§ã¯å°å 118px ã«ããããå ¥ããªãã®ã§åãçµã¿æ¿ãã 661 return ` 662 <div class="${classes.join(' ')}"> 663 <div class="store-main"> 664 <div class="store-info"> 665 <span class="store-rank">${i + 1}</span> 666 <div class="store-name${_hasSub ? ' has-subnote' : ''}"> 667 ${_nameHtml} 668 </div> 669 <span class="price-updated-m">${escapeHtml(p.updated)}</span> 670 </div> 671 <div class="store-mark-col">${p.mark_note ? `<span class="store-mark" data-tip="${escapeHtml(p.mark_note)}"><span class="store-mark-txt">${escapeHtml(p.mark_note)}</span></span>` : ''}${_campHtml}</div> 672 </div> 673 <div class="price-info"> 674 ${diffHtml}<span class="price-value">Â¥${deobfuscatePrice(p.price).toLocaleString()}</span>${(() => { 675 if (_msrp <= 0) return ''; 676 const _r = kaitoriRate(deobfuscatePrice(p.price), _msrp); 677 // rated ãªã¹ãå ã¯åãºã¬é²æ¢ã®ãã常ã«ã¹ãã³ãåºãï¼å¤ç¡ãã¯ä¸å¯è¦å ä½ï¼ 678 return `<span class="store-rate${_r === null ? ' none' : ''}" title="è²·åçï¼å®ä¾¡ Â¥${_msrp.toLocaleString()} æ¯ï¼">${_r === null ? '' : _r + '%'}</span>`; 679 })()} 680 <span class="price-updated">${escapeHtml(p.updated)}</span> 681 </div> 682 </div>`; 683 }).join('')}${kxLinkOnlyRowsHtml(product, prices.length)}</div>`; 684 685 // Product description (SEO content for users too) 686 const descEl = document.getElementById('productDescription'); 687 if (descEl) { 688 descEl.innerHTML = product.description_html || ''; 689 } 690 691 // Related products 692 const relatedEl = document.getElementById('relatedProducts'); 693 if (relatedEl) { 694 if (product.related && product.related.length > 0) { 695 let relHtml = '<div class="related-products">'; 696 relHtml += '<h3 class="related-title">é¢é£åå</h3>'; 697 relHtml += '<div class="related-grid">'; 698 product.related.forEach(function(r) { 699 const img = r.image_url 700 ? '<img src="' + escapeHtml(r.image_url) + '" alt="' + escapeHtml(r.name) + '" loading="lazy" width="80" height="80">' 701 : '<div class="related-img-placeholder">'+placeholderSVG(32)+'</div>'; 702 const priceHtml = r.max_price > 0 703 ? '<div class="related-price">æé« Â¥' + r.max_price.toLocaleString() + '</div>' 704 : ''; 705 relHtml += '<a href="/dp/' + escapeHtml(r.jan) + '" data-related-jan="' + escapeHtml(r.jan) + '" class="related-card">'; 706 relHtml += img; 707 relHtml += '<div class="related-name">' + escapeHtml(r.name) + '</div>'; 708 relHtml += priceHtml; 709 relHtml += '</a>'; 710 }); 711 relHtml += '</div></div>'; 712 relatedEl.innerHTML = relHtml; 713 // Event delegation for related product clicks 714 relatedEl.querySelectorAll('a[data-related-jan]').forEach(function(a) { 715 a.addEventListener('click', function(e) { 716 e.preventDefault(); 717 currentFrom = 'related'; 718 _updateBackBtn('related'); 719 showProduct(this.getAttribute('data-related-jan'), true); 720 }); 721 }); 722 } else { 723 relatedEl.innerHTML = ''; 724 } 725 } 726 727 // Deal ãã¼ã¸ãªã³ã¯ï¼share ã¡ãã¥ã¼å ãASIN ãããå ´åã®ã
727¿è¡¨ç¤ºï¼ 728 const shareDealEl = document.getElementById('shareDeal'); 729 if (shareDealEl) { 730 if (product.asin) { 731 shareDealEl.href = '/deal/' + escapeHtml(product.asin); 732 shareDealEl.style.display = ''; 733 } else { 734 shareDealEl.style.display = 'none'; 735 } 736 } 737 } 738 739 // ==================== æ´ä»£æé«è²·åããã㯠==================== 740 // mobile ã®é廿é«ã¯ãã¿ã¤ã«ç´ä¸ã®æèããã«ãï¼#statsCtxï¼ã® 1 è¡ã§ãå®ä¾¡ã¹ããªããã¨æ ãå ±æããã 741 // è¡ã 1 ã¤ãç¡ãæã¯æ ãã¨æ¶ãã両æ¹ããæã ãéã«ç½«ç·ãå¼ãï¼2026-09-24ï¼ã 742 function syncStatsCtx() { 743 const box = document.getElementById('statsCtx'); 744 if (!box) return; 745 const peakOn = !!document.getElementById('peakPriceSectionMobile')?.classList.contains('loaded'); 746 const msrp = document.getElementById('statMsrp'); 747 const msrpOn = !!msrp && msrp.style.display !== 'none'; 748 box.classList.toggle('has-peak', peakOn); 749 box.classList.toggle('is-empty', !peakOn && !msrpOn); 750 } 751 // 'YYYY-MM-DD' â ä»å¹´ãªã 'M/D'ããã以å¤ã¯ 'YYYY/M/D'ï¼mobile ã® 1 è¡ã«åããããï¼ 752 function _peakShortDate(s) { 753 const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s || ''); 754 if (!m) return s || ''; 755 return (+m[1] === new Date().getFullYear() ? '' : m[1] + '/') + (+m[2]) + '/' + (+m[3]); 756 } 757 function renderPeakPrice(peak) { 758 const pc = document.getElementById('peakPriceSection'); 759 const mob = document.getElementById('peakPriceSectionMobile'); 760 const notePc = document.getElementById('peakExcludedNote'); 761 const noteMob = document.getElementById('peakExcludedNoteMobile'); 762 if (!peak || !peak.price) { 763 if (pc) pc.style.display = 'none'; 764 if (mob) mob.classList.remove('loaded'); 765 if (notePc) notePc.style.display = 'none'; 766 if (noteMob) noteMob.style.display = 'none'; 767 syncStatsCtx(); 768 return; 769 } 770 // å¤ãå¤é¤å¤éç¥ (ä¾: ååºã®å ¥åãã¹ã§çªçºçã«é«é¡ã«ãªã£ãæ¥ãé¤å¤ããå ´å) 771 const excludedText = (peak.excluded && peak.excluded.price) 772 ? `â»Â¥${peak.excluded.price.toLocaleString()} ã¨ããè¨é²ãããã¾ããããå ¥åãã¹ã®å¯è½æ§ãããããé¤å¤ãã¦ãã¾ã` 773 : ''; 774 if (notePc) { 775 if (excludedText) { notePc.textContent = excludedText; notePc.style.display = ''; } 776 else notePc.style.display = 'none'; 777 } 778 if (noteMob) { 779 if (excludedText) { noteMob.textContent = excludedText; noteMob.style.display = ''; } 780 else noteMob.style.display = 'none'; 781 } 782 783 const prices = (currentProductData && currentProductData.prices) || []; 784 const currentMax = prices.length ? deobfuscatePrice(prices[0].price) : 0; 785 const peakStr = `Â¥${peak.price.toLocaleString()}`; 786 const dateStr = peak.date || ''; // YYYY-MM-DD 787 788 // ããã¼ã¯æ¯ã= ç¾å¨ - ãã¼ã¯ãâ ã§ä¸è½ãâ ã§ãã¼ã¯æ´æ°ã 789 let diffText = '', diffClass = 'same'; 790 if (currentMax > 0) { 791 const diff = currentMax - peak.price; 792 const dropPct = peak.price ? Math.round((Math.abs(diff) / peak.price) * 1000) / 10 : 0; 793 if (diff > 0) { 794 diffText = `â æ´æ°ä¸ +Â¥${diff.toLocaleString()}`; 795 diffClass = 'up'; 796 } else if (diff < 0) { 797 diffText = `â Â¥${Math.abs(diff).toLocaleString()} (${dropPct}%)`; 798 diffClass = 'down'; 799 } else { 800 diffText = '= ãã¼ã¯ä¸'; 801 diffClass = 'same'; 802 } 803 } 804 805 if (pc) { 806 pc.style.display = ''; 807 const v = document.getElementById('peakPriceValue'); 808 const d = document.getElementById('peakPriceDate'); 809 const b = document.getElementById('peakPctBadge'); 810 if (v) v.textContent = peakStr; 811 if (d) d.textContent = dateStr; 812 if (b) { b.textContent = diffText; b.className = 'peak-diff ' + diffClass; } 813 } 814 if (mob) { 815 mob.classList.add('loaded'); 816 const vm = document.getElementById('peakPriceValueMobile'); 817 const dm = document.getElementById('peakPriceDateMobile'); 818 const bm = document.getElementById('peakPctBadgeMobile'); 819 if (vm) vm.textContent = peakStr; 820 if (dm) { dm.textContent = _peakShortDate(dateStr); dm.title = dateStr; } 821 if (bm) { bm.textContent = diffText; bm.className = 'peak-diff ' + diffClass; } 822 } 823 syncStatsCtx(); 824 } 825 826 // ==================== å£²åºææ©ã¤ã³ã¸ã±ã¼ã¿ã¼ ==================== 827 function renderPriceIndicator(indicator) { 828 const badge = document.getElementById('priceIndicatorBadge'); 829 const label = document.getElementById('indicatorLabel'); 830 if (!badge || !label || !indicator || indicator === 'unknown') { 831 if (badge) badge.style.display = 'none'; 832 return; 833 } 834 const config = { 835 high: { text: 'ð é«å¤å â ä»ã売ãæï¼', cls: 'indicator-high' }, 836 low: { text: 'ð å®å¤å â æ§åè¦æ¨å¥¨', cls: 'indicator-low' }, 837 normal: { text: 'ð éå¸¸ä¾¡æ ¼å¸¯', cls: 'indicator-normal' } 838 }; 839 const c = config[indicator]; 840 if (!c) { badge.style.display = 'none'; return; } 841 label.textContent = c.text; 842 label.className = c.cls; 843 badge.style.display = ''; 844 } 845 846 // ==================== ã¾ã¨ãURLå ±æ ==================== 847 function encodeMatomToUrl() { 848 const matome = getMatomeFromStorage(); 849 if (!matome || matome.items.length === 0) return null; 850 const param = matome.items.map(item => item.jan + ':' + (item.quantity || 1)).join(','); 851 const url = new URL(window.location.origin); 852 url.searchParams.set('m', param); 853 url.searchParams.set('matome', 'compare'); 854 return url.toString(); 855 } 856 857 function decodeMatomFromUrl() { 858 const params = new URLSearchParams(window.location.search); 859 const m = params.get('m'); 860 if (!m) return null; 861 const items = []; 862 m.split(',').forEach(part => { 863 const [jan, qty] = part.split(':'); 864 if (jan && jan.match(/^\d{7,14}$/)) { 865 items.push({ jan, quantity: parseInt(qty) || 1 }); 866 } 867 }); 868 return items.length > 0 ? items : null; 869 } 870 871 // ==================== Chart.js å¤é¨ HTML tooltip (å¤åºè対å¿) ==================== 872 // canvas å æç»ã ã¨è²·ååºãå¤ãæä¸é¨ã clip ããããHTML tooltip ã§ max-height + 2 åã¬ã¤ã¢ã¦ããå®ç¾ã 873 // ð´ ã¿ãã端æ«ã«ã¯ mouseout ãç¡ãã®ã§ãä¸åº¦åºãã tooltip ã**åºãã£ã±ãªã**ã« 874 // ãªãï¼May ææ 2026-09-05ï¼ãã°ã©ãã®å¤ã触ã£ããéããã 875 // éããã®ã¯ DOM ãé ãã ãã§ã¯è¶³ããªãââChart.js å´ã® active è¦ç´ ã 876 // æ¶ããªãã¨ã次ã®åæç»ï¼æé夿´ã»å¡ä¾ãã°ã«ã»iOS ã® canvas 復å ï¼ã§ 877 // åã tooltip ãã¾ãåºã¦ããã 878 function _bindChartTooltipDismiss() { 879 if (document.body.dataset.kxTipDismiss) return; // ãªã¹ã㯠1 åã ã 880 document.body.dataset.kxTipDismiss = '1'; 881 function dismiss(ev) { 882 const el = document.getElementById('chart-tooltip-ext'); 883 if (!el || el.style.opacity === '0') return; // åºã¦ããªããã°ä½ãããªã 884 const cv = document.getElementById('priceChart');
885 // ã°ã©ãæ¬ä½ã®ã¿ãã㯠Chart.js ã«ä»»ããï¼å¥ã®æ¥ä»ã¸ç§»ãããï¼ 886 if (ev && ev.target && cv && cv.contains(ev.target)) return; 887 el.style.opacity = '0'; 888 if (!priceChart) return; 889 try { 890 priceChart.setActiveElements([]); 891 if (priceChart.tooltip) priceChart.tooltip.setActiveElements([], { x: 0, y: 0 }); 892 priceChart.update('none'); 893 } catch (_) {} 894 } 895 document.addEventListener('touchstart', dismiss, { passive: true, capture: true }); 896 document.addEventListener('click', dismiss, true); 897 } 898 899 function chartTooltipExternal(context) { 900 const { chart, tooltip } = context; 901 let el = document.getElementById('chart-tooltip-ext'); 902 if (!el) { 903 el = document.createElement('div'); 904 el.id = 'chart-tooltip-ext'; 905 el.style.cssText = 'position:absolute;pointer-events:none;z-index:9999;opacity:0;transition:opacity 0.1s;' 906 + 'background:var(--bg-card,#fff);color:var(--text-primary,#0F172A);' 907 + 'border:1px solid var(--border,#E2E8F0);border-radius:10px;padding:12px 14px;' 908 + 'box-shadow:0 8px 24px rgba(0,0,0,0.12);font-size:0.85rem;line-height:1.45;' 909 + 'box-sizing:border-box;'; 910 document.body.appendChild(el); 911 } 912 if (!tooltip || tooltip.opacity === 0) { el.style.opacity = '0'; return; } 913 914 const title = (tooltip.title && tooltip.title[0]) || ''; 915 const items = (tooltip.dataPoints || []) 916 .filter(d => d.parsed && d.parsed.y !== null && d.parsed.y !== undefined) 917 .sort((a, b) => (b.parsed.y || 0) - (a.parsed.y || 0)); 918 // tooltip æå¤§å¹ ã viewport å¹ ããåçã«ç®åºï¼å·¦å³ 24px ä½ç½ï¼ 919 const tipMaxW = Math.min(window.innerWidth - 24, 1100); 920 el.style.maxWidth = tipMaxW + 'px'; 921 // åå¹ ã¯æé·åºåï¼ã¢ãã¤ã«ããã¯ã¹: Â¥XXX,XXX çï¼ãæãè¿ããªãå®å¯¸ â 200px 922 // auto-fit + minmax ã§ãç»é¢å¹ / 220ãã§åæ°èªåæ±ºå® â çãç»é¢ 1 åãåºãç»é¢ 4-5 å 923 const gridCss = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));column-gap:14px;row-gap:3px;'; 924 const rows = items.map(it => { 925 const c = it.dataset.borderColor || '#94A3B8'; 926 const label = (it.dataset.label || '').replace(/[&<>"]/g, ch => ({'&':'&','<':'<','>':'>','"':'"'}[ch])); 927 return `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">` 928 + `<span style="display:inline-block;width:8px;height:8px;background:${c};border-radius:50%;flex-shrink:0;"></span>` 929 + `<span style="overflow:hidden;text-overflow:ellipsis;">${label}: Â¥${it.parsed.y.toLocaleString()}</span></div>`; 930 }).join(''); 931 el.innerHTML = `<div style="font-weight:700;margin-bottom:6px;">${title}</div><div style="${gridCss}">${rows}</div>`; 932 933 const rect = chart.canvas.getBoundingClientRect(); 934 const left = window.scrollX + rect.left + tooltip.caretX; 935 const top = window.scrollY + rect.top + tooltip.caretY; 936 // å³è¾º / ä¸è¾ºãã¯ã¿åºããªããã調æ´ï¼ã¿ããå¾ã®äºåç®æç»ã§æ£ç¢ºã«ãªãï¼ 937 el.style.opacity = '1'; 938 el.style.left = '0px'; 939 el.style.top = '0px'; 940 el.style.transform = ''; 941 const w = el.offsetWidth, h = el.offsetHeight; 942 const maxLeft = window.scrollX + window.innerWidth - w - 12; 943 const maxTop = window.scrollY + window.innerHeight - h - 12; 944 el.style.left = Math.min(Math.max(left + 14, window.scrollX + 6), Math.max(maxLeft, window.scrollX + 6)) + 'px'; 945 el.style.top = Math.min(Math.max(top - h / 2, window.scrollY + 6), Math.max(maxTop, window.scrollY + 6)) + 'px'; 946 } 947
948 // ==================== å¾è¡¨æ¸²æ (éé æäº®æ¨¡å¼) ==================== 949 // ==================== ãã£ã¼ã表示æéã®ããã©ã«ã/è¨æ¶ ==================== 950 // ã¦ã¼ã¶ã¼ãæéãé¸ãã ã localStorage ã«è¨æ¶ãæªè¨å®æã¯ PC=30æ¥ / ã¢ãã¤ã«=14æ¥ã 951 const _CHART_PERIOD_OPTS = ['7', '14', '30', '90', '180']; 952 function _chartPeriodPref() { 953 try { 954 const v = localStorage.getItem('kx_chart_period'); 955 if (v && _CHART_PERIOD_OPTS.indexOf(v) >= 0) return parseInt(v, 10); 956 } catch (e) {} 957 const isMobile = window.matchMedia && window.matchMedia('(max-width: 768px)').matches; 958 return isMobile ? 14 : 30; 959 } 960 function _saveChartPeriod(days) { 961 try { localStorage.setItem('kx_chart_period', String(days)); } catch (e) {} 962 } 963 964 // ==================== ã»ã¼ã«æé帯ï¼ãã£ã¼ãèæ¯ï¼ ==================== 965 let _salePeriods = null; 966 let _showSaleBands = localStorage.getItem('kx_chart_sale') !== '0'; 967 // ãã©ãããã©ã¼ã å¥ãã£ã«ã¿ã¼ï¼å¡ä¾ããããã¯ãªãã¯ã§åæ¿ã2026-06-11ï¼ 968 const _saleTypes = { amazon: true, rakuten: true }; 969 try { 970 const _stv = localStorage.getItem('kx_chart_sale_types'); 971 if (_stv !== null) { _saleTypes.amazon = _stv.indexOf('amazon') >= 0; _saleTypes.rakuten = _stv.indexOf('rakuten') >= 0; } 972 } catch (e) {} 973 async function ensureSalePeriods() {
974 if (_salePeriods !== null) return; 975 _salePeriods = []; 976 try { 977 const d = await apiCall('/api/sale-periods'); 978 _salePeriods = (d && d.periods) || []; 979 if (priceChart) priceChart.update(); 980 _syncSaleBtn(); 981 } catch (e) { /* 帯ã¯é£¾ã */ } 982 } 983 function _syncSaleBtn() { 984 const btn = document.getElementById('chartSaleBtn'); 985 if (btn) btn.classList.toggle('active', _showSaleBands); 986 // ã»ã¼ã« ON ã®ã¨ãã ããã©ãããã©ã¼ã ãããè¡ãåºãï¼OFF æã¯è¡ãã¨æ¶ãã¦å ´æãåããªãï¼ 987 const note = document.getElementById('chartSaleNote'); 988 if (note) note.style.display = _showSaleBands ? '' : 'none'; 989 const ba = document.getElementById('saleTypeAmazon'); 990 const br = document.getElementById('saleTypeRakuten'); 991 if (ba) ba.setAttribute('aria-pressed', _saleTypes.amazon ? 'true' : 'false'); 992 if (br) br.setAttribute('aria-pressed', _saleTypes.rakuten ? 'true' : 'false'); 993 } 994 // 表示ä¸ãã£ã¼ãã®ã»ã¼ã«æé â å¯è¦ç¯å²ã»ãã¯ã»ã«åº§æ¨ã解決ï¼å¸¯ã¨ãã¼ã«ã¼ã§å ±ç¨ï¼ 995 function _resolveSaleBands(chart) { 996 if (!_showSaleBands || !_salePeriods || !_salePeriods.length) return []; 997 const labels = chart.data.labels || []; 998 if (!labels.length) return []; 999 // MM/DD ã©ãã« â å¹´æ¨å®ä»ã Dateï¼æå¤§180æ¥çªãªã®ã§å¹´è·¨ãã¯åå¹´å¤å®ã§OKï¼ 1000 const now = new Date(), cy = now.getFullYear(), cm = now.getMonth() + 1; 1001 const dates = labels.map(l => { 1002 const p = String(l).split('/').map(Number); 1003 if (p.length < 2 || isNaN(p[0])) return null; 1004 return new Date(p[0] > cm ? cy - 1 : cy, p[0] - 1, p[1]); 1005 }); 1006 const x = chart.scales.x, area = chart.chartArea; 1007 const out = []; 1008 _salePeriods.forEach(p => { 1009 if (!_saleTypes[p.type === 'rakuten' ? 'rakuten' : 'amazon']) return; 1010 const s = new Date(p.start + 'T00:00:00'); 1011 const e = new Date((p.end || new Date().toISOString().slice(0, 10)) + 'T23:59:59'); 1012 let i0 = -1, i1 = -1; 1013 dates.forEach((d, i) => { 1014 if (!d) return; 1015 if (d >= s && d <= e) { if (i0 < 0) i0 = i; i1 = i; } 1016 }); 1017 if (i0 < 0) return; 1018 const half = labels.length > 1 ? (x.getPixelForValue(1) - x.getPixelForValue(0)) / 2 : 20; 1019 const px0 = Math.max(area.left, x.getPixelForValue(i0) - half); 1020 const px1 = Math.min(area.right, x.getPixelForValue(i1) + half); 1021 out.push({ p, px0, px1, mx: x.getPixelForValue(i0) }); 1022 }); 1023 return out; 1024 } 1025 function _saleDateShort(ymd) { 1026 const m = String(ymd || '').match(/^\d{4}-(\d{2})-(\d{2})/); 1027 return m ? `${Number(m[1])}/${Number(m[2])}` : (ymd || ''); 1028 } 1029 // ãã¼ã¿ç¹ã1æ¥åããç¡ãç³»åï¼ä¾: 仿¥åãã¦ä¾¡æ ¼ãä»ããè²·åwikiï¼ã¯å¤ç«ãã 1030 // å°ããªç¹ã«ãªãè¦è½ã¨ããããããç¹ã®å·¦ã¸åè²ã®çãå®ç·ãå¼ãã¦è¦èªæ§ãä¸ããã 1031 const _singlePointStubPlugin = { 1032 id: 'kxSinglePointStub', 1033 afterDatasetsDraw(chart) { 1034 const ctx = chart.ctx; 1035 const area = chart.chartArea; 1036 chart.data.datasets.forEach((ds, i) => { 1037 const meta = chart.getDatasetMeta(i); 1038 if (meta.hidden) return; 1039 // énullï¼å®ãã¼ã¿ï¼ã®ç¹ã¤ã³ããã¯ã¹ãåé 1040 let only = -1, count = 0; 1041 (ds.data || []).forEach((v, idx) => { if (v != null) { count++; only = idx; } }); 1042 if (count !== 1) return; // åä¸ãã¼ã¿ç¹ã®ç³»åã ã対象 1043 const el = meta.data && meta.data[only]; 1044 if (!el) return; 1045 const x = el.x, y = el.y; 1046 const x0 = Math.max(area.left, x - 24); // å·¦ã¸æå¤§24pxï¼è»¸å ã«åããï¼ 1047 if (x0 >= x) return; 1048 ctx.save(); 1049 ctx.beginPath(); 1050 ctx.strokeStyle = ds.borderColor || '#94A3B8'; 1051 ctx.lineWidth = ds.borderWidth || 2.5; 1052 ctx.lineCap = 'round'; 1053 ctx.moveTo(x0, y); 1054 ctx.lineTo(x, y); // ç¹ã®ä¸å¿ã¾ã§å¼ãã¦ãââãã«è¦ã
1054ã 1055 ctx.stroke(); 1056 ctx.restore(); 1057 }); 1058 } 1059 }; 1060 const _saleBandPlugin = { 1061 id: 'kxSaleBands', 1062 beforeDatasetsDraw(chart) { 1063 const bands = _resolveSaleBands(chart); 1064 if (!bands.length) { chart.$kxSaleMarkers = []; return; } 1065 const area = chart.chartArea, ctx = chart.ctx; 1066 const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 1067 ctx.save(); 1068 const markers = []; 1069 bands.forEach(({ p, px0, px1, mx }) => { 1070 ctx.fillStyle = p.type === 'rakuten' 1071 ? (isDark ? 'rgba(248,113,113,0.10)' : 'rgba(191,0,0,0.07)') 1072 : (isDark ? 'rgba(255,153,0,0.12)' : 'rgba(255,153,0,0.09)'); 1073 ctx.fillRect(px0, area.top, px1 - px0, area.bottom - area.top); 1074 // æ¥ä»è»¸ä¸ã®ã¤ãã³ããã¼ã«ã¼ï¼TradingView 風ï¼: æééå§ä½ç½®ã« â² 1075 markers.push({ 1076 x: Math.max(area.left + 5, Math.min(mx, area.right - 5)), 1077 y: area.bottom + 4, 1078 p 1079 }); 1080 }); 1081 chart.$kxSaleMarkers = markers; 1082 ctx.restore(); 1083 }, 1084 afterDraw(chart) { 1085 const markers = chart.$kxSaleMarkers || []; 1086 if (!markers.length) return; 1087 const ctx = chart.ctx; 1088 const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 1089 ctx.save(); 1090 markers.forEach(m => { 1091 const color = m.p.type === 'rakuten' ? (isDark ? '#F87171' : '#BF0000') : '#FF9900'; 1092 ctx.beginPath(); 1093 ctx.moveTo(m.x, m.y); 1094 ctx.lineTo(m.x - 5, m.y + 8); 1095 ctx.lineTo(m.x + 5, m.y + 8); 1096 ctx.closePath(); 1097 ctx.fillStyle = color; 1098 ctx.fill(); 1099 ctx.lineWidth = 1; 1100 ctx.strokeStyle = isDark ? 'rgba(15,23,42,0.9)' : '#fff'; 1101 ctx.stroke(); 1102 }); 1103 // hover/ã¿ããä¸ã®ãã¼ã«ã¼ã¯æéå tooltip ãæç» 1104 const hov = chart.$kxSaleMarkerHover; 1105 if (hov) { 1106 const name = hov.p.name || (hov.p.type === 'rakuten' ? '楽天ã»ã¼ã«' : 'Amazonã»ã¼ã«'); 1107 const range = `${_saleDateShort(hov.p.start)}ã${_saleDateShort(hov.p.end)}`; 1108 ctx.font = '600 12px sans-serif'; 1109 const w1 = ctx.measureText(name).width; 1110 ctx.font = '11px sans-serif'; 1111 const w2 = ctx.measureText(range).width; 1112 const bw = Math.max(w1, w2) + 20, bh = 40; 1113 const area = chart.chartArea; 1114 let bx = Math.max(area.left, Math.min(hov.x - bw / 2, area.right - bw)); 1115 let by = hov.y - bh - 8; // ãã¼ã«ã¼ä¸ã«è¡¨ç¤ºï¼è»¸ã©ãã«ã¨éãªããªãï¼ 1116 ctx.fillStyle = isDark ? 'rgba(15,23,42,0.95)' : 'rgba(255,255,255,0.97)'; 1117 ctx.strokeStyle = isDark ? '#334155' : '#E2E8F0'; 1118 ctx.lineWidth = 1; 1119 ctx.beginPath(); 1120 ctx.roundRect(bx, by, bw, bh, 6); 1121 ctx.fill(); 1122 ctx.stroke(); 1123 ctx.fillStyle = isDark ? '#E2E8F0' : '#1E293B'; 1124 ctx.font = '600 12px sans-serif'; 1125 ctx.fillText(name, bx + 10, by + 16); 1126 ctx.fillStyle = isDark ? '#94A3B8' : '#64748B'; 1127 ctx.font = '11px sans-serif'; 1128 ctx.fillText(range, bx + 10, by + 32); 1129 } 1130 ctx.restore(); 1131 }, 1132 afterEvent(chart, args) { 1133 const markers = chart.$kxSaleMarkers || []; 1134 const ev = args.event; 1135 if (!ev || (ev.type !== 'mousemove' && ev.type !== 'click' && ev.type !== 'mouseout')) return; 1136 let hit = null; 1137 if (ev.type !== 'mouseout' && ev.x != null) { 1138 for (const m of markers) { 1139 if (Math.abs(ev.x - m.x) <= 8 && ev.y >= m.y - 4 && ev.y <= m.y + 14) { hit = m; break; } 1140 } 1141 } 1142 if (hit !== (chart.$kxSaleMarkerHover || null)) { 1143 chart.$kxSaleMarkerHover = hit; 1144 args.changed = true; // åæç»ã㦠tooltip ãåæ 1145 } 1146 if (chart.canvas) chart.canvas.style.cursor = hit ? 'pointer' : ''; 1147 } 1148 }; 1149 1150 // ==================== æéãã©ã·ï¼ãã£ã¼ãä¸ã®ãããããç¯å²é¸æï¼ ==================== 1151 let _brushState = null; // { len, start, end } 1152 const _BRUSH_MIN_SPAN = 2; // æå°ã¦ã£ã³ã㦠= 3ãã¤ã³ã 1153 function renderBrush(history) { 1154 const brush = document.getElementById('chartBrush'); 1155 if (!brush) return; 1156 if (!history || history.length < 8) { brush.style.display = 'none'; return; } 1157 brush.style.display = ''; 1158 _drawBrushMini(history); 1159 _syncBrushWin(); 1160 } 1161 function _drawBrushMini(history) {
1162 const c = document.getElementById('brushMini'); 1163 if (!c || !history || !history.length) return; 1164 // ð´ 親ãé表示ã 㨠clientWidth 㯠0ã`|| 300` ã§ä»£ç¨ãã㨠**300px ã® 1165 // æç»ãããã¡**ãä½ãããCSS ã® width:100% ã§å®å¹ ã¸å¼ã伸ã°ãã㦠1166 // ã¼ãããï¼å±¥æ´ãã¥ã¼ä¸ã¯ #chartBrush ã display:none ãªã®ã§ã 1167 // ãã®ç¶æ ã§ãã¼ããåãæ¿ããã¨èµ·ããï¼ãæããã«å¸°ãã®ãæ£è§£ââ 1168 // è¡¨ç¤ºã«æ»ãæã¯ renderChart ãæãç´ãã 1169 if (!c.parentElement || c.parentElement.clientWidth === 0) return; 1170 const w = c.parentElement.clientWidth, h = c.parentElement.clientHeight || 36; 1171 const dpr = window.devicePixelRatio || 1; 1172 c.width = Math.round(w * dpr); c.height = Math.round(h * dpr); 1173 const ctx = c.getContext('2d'); 1174 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 1175 ctx.clearRect(0, 0, w, h); 1176 // åæ¥ã®æé«å¤ã©ã¤ã³ 1177 const vals = history.map(d => { 1178 let m = null; 1179 Object.values(d.prices || {}).forEach(v => { if (v && (m === null || v > m)) m = v; }); 1180 return m; 1181 }); 1182 const nums = vals.filter(v => v !== null); 1183 if (!nums.length) return; 1184 let lo = Math.min(...nums), hi = Math.max(...nums); 1185 if (hi === lo) { hi += 1; lo -= 1; } 1186 const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 1187 const px = i => history.length > 1 ? (i / (history.length - 1)) * w : w / 2; 1188 const py = v => 4 + (1 - (v - lo) / (hi - lo)) * (h - 8); 1189 ctx.beginPath(); 1190 let started = false; 1191 vals.forEach((v, i) => { 1192 if (v === null) return; 1193 if (!started) { ctx.moveTo(px(i), py(v)); started = true; } 1194 else ctx.lineTo(px(i), py(v)); 1195 }); 1196 ctx.strokeStyle = isDark ? 'rgba(147,197,253,0.7)' : 'rgba(37,99,235,0.55)'; 1197 ctx.lineWidth = 1.5; 1198 ctx.stroke(); 1199 ctx.lineTo(px(history.length - 1), h); ctx.lineTo(px(0), h); ctx.closePath(); 1200 ctx.fillStyle = isDark ? 'rgba(147,197,253,0.10)' : 'rgba(37,99,235,0.08)'; 1201 ctx.fill(); 1202 } 1203 function _syncBrushWin() { 1204 if (!_brushState) return; 1205 const win = document.getElementById('brushWin'); 1206 const dl = document.getElementById('brushDimL'); 1207 const dr = document.getElementById('brushDimR'); 1208 if (!win) return; 1209 const n = Math.max(1, _brushState.len - 1); 1210 const l = (_brushState.start / n) * 100, r = (_brushState.end / n) * 100; 1211 win.style.left = l + '%'; 1212 win.style.width = Math.max(0, r - l) + '%'; 1213 if (dl) dl.style.width = l + '%'; 1214 if (dr) dr.style.width = (100 - r) + '%'; 1215 } 1216 function _applyBrushToChart() { 1217 if (!_brushState || !currentHistoryData) return; 1218 renderChart(currentHistoryData, 0, { start: _brushState.start, end: _brushState.end }); 1219 // æéã»ã¬ã¯ãã®åæï¼æ«å°¾ N æ¥ã¨ä¸è´ããã°ãã®å¤ããã以å¤ã¯ãã«ã¹ã¿ã ãï¼ 1220 const span = _brushState.end - _brushState.start + 1; 1221 const atEnd = _brushState.end === _brushState.len - 1; 1222 const sel = document.getElementById('chartRangeSel'); 1223 if (!sel) return; 1224 if (atEnd && _brushState.start === 0) sel.value = '180'; // å ¨ç¯å² = å ¨æé 1225 else if (atEnd && [7, 14, 30, 90].indexOf(span) >= 0) sel.value = String(span); 1226 else sel.value = '0'; 1227 } 1228 (function bindBrush() { 1229 let drag = null; // { mode:'l'|'r'|'win', startX, s0, e0, rect } 1230 let raf = 0; 1231 function idxFromDx(dx, rect) { 1232 if (!_brushState) return 0; 1233 return Math.round(dx / Math.max(1, rect.width) * (_brushState.len - 1)); 1234 } 1235 function onDown(e) { 1236 const t = e.target; 1237 if (!_brushState) return; 1238 const brush = document.getElementById('chartBrush'); 1239 if (!brush || !brush.contains(t)) return; 1240 const mode = t.id === 'brushHL' ? 'l' : t.id === 'brushHR' ? 'r' : (t.id === 'brushWin' ? 'win' : null); 1241 if (!mode) return; 1242 e.preventDefault(); 1243 drag = { mode, startX: e.clientX, s0: _brushState.start, e0: _brushState.end, rect: brush.getBoundingClientRect() }; 1244 try { t.setPointerCapture(e.pointerId); } catch (err) {} 1245 } 1246 function onMove(e) { 1247 if (!drag || !_brushState) return; 1248 const di = idxFromDx(e.clientX - drag.startX, drag.rect); 1249 const len = _brushState.len; 1250 let s = drag.s0, en = drag.e0; 1251 if (drag.mode === 'l') s = Math.min(Math.max(0, drag.s0 + di), en - _BRUSH_MIN_SPAN); 1252 else if (drag.mode === 'r') en = Math.max(Math.min(len - 1, drag.e0 + di), s + _BRUSH_MIN_SPAN); 1253 else { // win: å¹ ãä¿ã£ãã¾ã¾ãã³ 1254 const spanW = drag.e0 - drag.s0; 1255 s = Math.min(Math.max(0, drag.s0 + di), len - 1 - spanW); 1256 en = s + spanW; 1257 } 1258 if (s === _brushState.start && en === _brushState.end) return; 1259 _brushState.start = s; _brushState.end = en; 1260 _syncBrushWin(); 1261 if (!raf) raf = requestAnimationFrame(() => { raf = 0; _applyBrushToChart(); }); 1262 } 1263 function onUp() { drag = null; } 1264 document.addEventListener('pointerdown', onDown); 1265 document.addEventListener('pointermove', onMove); 1266 document.addEventListener('pointerup', onUp); 1267 document.addEventListener('pointercancel', onUp); 1268 let rsz = 0; 1269 window.addEventListener('resize', () => { 1270 clearTimeout(rsz); 1271 rsz = setTimeout(() => { if (currentHistoryData) { _drawBrushMini(currentHistoryData); _syncBrushWin(); } }, 200); 1272 }); 1273 })(); 1274
1275 // æå®æ¥ï¼ISOï¼æç¹ã§æå¹ãªå®ä¾¡ãç¡ããã° nullã 1276 function msrpAt(dateIso) { 1277 if (!currentMsrpHistory || !dateIso) return null; 1278 let v = null; 1279 for (const e of currentMsrpHistory) { 1280 if (e.from <= dateIso) v = e.msrp; else break; 1281 } 1282 return v; 1283 } 1284 1285 // è²·åç = è²·åä¾¡æ ¼ ÷ å®ä¾¡ï¼%表示ãå°æ°1æ¡ï¼ 1286 function kaitoriRate(price, msrp) { 1287 if (!msrp || !price || price <= 0) return null; 1288 return Math.round(price / msrp * 1000) / 10; 1289 } 1290 1291 // ==================== ä¾¡æ ¼å¤åå±¥æ´ï¼ç´è¿14æ¥ã»2026-09-05ï¼==================== 1292 // ã°ã©ãã¨åãã«ã¼ãã® 2 æç®ãæºã¯ price_updatesï¼scanner ãååã¨éãå¤ã 1293 // æ¸ããæã ã 1 è¡ç©ãã¤ãã³ããã°ï¼ï¼**åããç¬éããè¡ãç¡ã**ã 1294 // ð´ æ¥æ¬¡ã¹ãããã·ã§ããã§ä½ããªãçç±: æä¾¡ 24,935 JAN ã®ãã¡ç´è¿ 30 æ¥ã« 1295 // 1 度ã§ãåããã®ã¯ 11,432 ä»¶ï¼46%ï¼ã ããåããå´ã 78% 㯠â¤5 æ¥ãã 1296 // åããªãï¼æ¥æ¬¡è¡¨ã¯åå以ä¸ã®ååãã±0ãã®ç©ºè¡ã«ãªãï¼å®æ¸¬ 2026-09-05ï¼ã 1297 // ð´ è¨é²ã¯ç¡æéã»è¡¨ç¤ºã ã 14 æ¥ãçµãè¾¼ã¿ã¯ç«¯ç¹ã® WHERE ã¨ä¸ã® PLOG_DAYS 1298 // ã ãã«ãããprice_updates å´ã«ä¿ææéã¯æãã¦ããªãã 1299 var PLOG_DAYS = 14; 1300 var PLOG_PAGE = 20; // 1 ãã¼ã¸ã宿¸¬ 97 ä»¶ã®ååãããå ¨é¨åºãã¨èªããªã 1301 var _plogLoadedJan = null; // ã©ã® JAN ã®å±¥æ´ãåºãã¦ãããï¼ååãè·¨ãã åå©ç¨é²æ¢ï¼ 1302 var _productReadyJan = null; // `/api/product/{jan}` ã await æ¸ã¿ï¼marker ãæ¸ããã JAN 1303 var _plogAll = []; // åå¾ããå ¨ä»¶ï¼ãã£ã«ã¿åï¼ãååå¾ããã«çµãè¾¼ãããä¿æ 1304 var _plogPage = 1; // 1 å§ã¾ãã®ãã¼ã¸çªå· 1305 // ð´ å¡ä¾ã§**æç¤ºçã«æ¶ããã**åºã ããå ¥ããï¼= ã°ã©ãã®å ¨ key â è¡¨ç¤ºä¸ keyï¼ã 1306 // ã表示ä¸ã«ç¡ããã§å¤å®ãã¦ã¯ãããªã: å±¥æ´ã® 14 æ¥çªã«ã¯å± ããã°ã©ãã® 1307 // æéã«ã¯ç¹ãç¡ãåºï¼ä¸æ¶æ¸ã¿çï¼ãå± ã¦ããããåæã«é ããã¨ã«ãªãã 1308 var _plogHidden = new Set(); 1309 1310 function plogYen(n) { return 'Â¥' + Number(n).toLocaleString('ja-JP'); } 1311 1312 function _setPlogLive(text) { 1313 var live = document.getElementById('plogLive'); 1314 if (live) live.textContent = text; 1315 } 1316 1317 function _plogVisible() { 1318 if (!_plogHidden.size) return _plogAll; 1319 return _plogAll.filter(function (c) { return !_plogHidden.has(c.store_key); }); 1320 } 1321 1322 // ãã¼ã¸çªå·ãã¿ã³ãå ¨ãã¼ã¸åºã㨠97 ä»¶=5 ãã¼ã¸ç¨åº¦ã§ãæ¨ªã«æº¢ããã®ã§ã 1323 // å é ã»æ«å°¾ï¼ç¾å¨ãã¼ã¸ã®åå¾ 2 ã¤ã ãåºãã¦éã¯ãâ¦ãã§ç³ãã 1324 function _plogPagerHtml(totalPages) { 1325 if (totalPages <= 1) return ''; 1326 var cur = _plogPage; 1327 var want = new Set([1, totalPages]); 1328 for (var i = cur - 2; i <= cur + 2; i++) if (i >= 1 && i <= totalPages) want.add(i); 1329 var nums = Array.from(want).sort(function (a, b) { return a - b; }); 1330 var html = '<button type="button" class="plog-pg plog-pg-nav" data-p="' + (cur - 1) + '"' + 1331 (cur === 1 ? ' disabled' : '') + ' aria-label="åã®ãã¼ã¸">â¹</button>'; 1332 var prev = 0; 1333 nums.forEach(function (n) { 1334 if (prev && n - prev > 1) html += '<span class="plog-pg-gap">â¦</span>'; 1335 html += '<button type="button" class="plog-pg' + (n === cur ? ' active' : '') + 1336 '" data-p="' + n + '"' + (n === cur ? ' aria-current="page"' : '') + '>' + n + '</button>'; 1337 prev = n; 1338 }); 1339 html += '<button type="button" class="plog-pg plog-pg-nav" data-p="' + (cur + 1) + '"' + 1340 (cur === totalPages ? ' disabled' : '') + ' aria-label="次ã®ãã¼ã¸">âº</button>'; 1341 // æå ¥åã§ãã¼ã¸ãé£ã°ããçªå·ãç³ãã§ããï¼â¦ï¼ã¶ããé ããã¼ã¸ã¸ã¯ 1342 // ãããå¯ä¸ã® 1 æãinputmode=numeric ã§ã¢ãã¤ã«ã¯ãã³ãã¼ãåºãã 1343 html += '<span class="plog-pg-jump">' 1344 + '<input type="number" class="plog-pg-input" inputmode="numeric" min="1" max="' + totalPages + '"' 1345 + ' value="' + cur + '" aria-label="ãã¼ã¸çªå·ãå ¥å">' 1346 + '<span class="plog-pg-total">/ ' + totalPages + '</span></span>'; 1347 return '<div class="plog-pager" role="navigation" aria-label="ä¾¡æ ¼å¤åå±¥æ´ã®ãã¼ã¸">' + html + '</div>'; 1348 } 1349 1350 function renderPriceLog(reset) { 1351 var pane = document.getElementById('logPane'); 1352 if (!pane) return; 1353 var list = _plogVisible(); 1354 if (reset) _plogPage = 1; 1355 1356 // ð´ ç©ºè¡¨ç¤ºã®æããèªã¿ä¸ãã«ä¼ãããããã§ return ã㦠live é åã 1357 // ç½®ãå»ãã«ããã¨ãããã¹ã¦é表示ããæ¼ããèªã¿ä¸ãå©ç¨è ã«ã¯ 1358 // **åã®ãã¼ã¸ã®ä»¶æ°ãæ®ã£ãã¾ã¾**ä½ãç¥ãããã
1358ªãã 1359 if (!_plogAll.length) { 1360 var m0 = 'ç´è¿' + PLOG_DAYS + 'æ¥éãè²·åä¾¡æ ¼ã®å¤åã¯ããã¾ããã'; 1361 pane.innerHTML = '<div class="plog-empty">' + m0 + '</div>'; 1362 _setPlogLive(m0); 1363 return; 1364 } 1365 if (!list.length) { 1366 // ð´ å ¨åºãæ¶ããæããå¤åãç¡ããã¨åã空表示ã«ããã¨ãçµãè¾¼ã¿ã§ 1367 // 空ãªã®ãæ¬å½ã«åãã¦ããªãã®ãåºå¥ãä»ããªãã 1368 var m1 = '表示ä¸ã®åºèãããã¾ãããä¸ã®å¡ä¾ã§åºèãé¸ãã§ãã ããã'; 1369 pane.innerHTML = '<div class="plog-empty">' + m1 + '</div>'; 1370 _setPlogLive(m1); 1371 return; 1372 } 1373 1374 var totalPages = Math.max(1, Math.ceil(list.length / PLOG_PAGE)); 1375 // çµãè¾¼ã¿ã§ä»¶æ°ãæ¸ã£ã¦ç¾å¨ãã¼ã¸ãæ¶ãããã¨ãããï¼97 ä»¶ 5 ãã¼ã¸ã§ 1376 // 5 ãã¼ã¸ç®ã«ããæã« 1 åºã ãæ®ã㨠1 ãã¼ã¸ããç¡ããªãï¼ 1377 if (_plogPage > totalPages) _plogPage = totalPages; 1378 if (_plogPage < 1) _plogPage = 1; 1379 var from = (_plogPage - 1) * PLOG_PAGE; 1380 var to = Math.min(from + PLOG_PAGE, list.length); 1381 1382 var rows = list.slice(from, to).map(function (c) { 1383 var when = '<div class="plog-when">' + escapeHtml((c.at || '').slice(5)) + '</div>'; 1384 var store = '<span class="plog-store">' + escapeHtml(c.store) + '</span>'; 1385 var move, tag; 1386 if (c.kind === 'listed') { 1387 // ð´ 0 ã¯ä¾¡æ ¼ã§ã¯ãªãããã®åºã«æ²è¼ãç¡ããç¶æ ãå¼ãç®ã㦠1388 // â²192,600 ã®æ´é¨°ã¨ãã¦åºããªãï¼ç´è¿ 14 æ¥ 81,148 è¡ã®ãã¡ 1389 // æ°è¦ 16,598ã»çµäº 12,013ï¼35% ã 0 絡ã¿ï¼ã¨ãã¸ã±ã¼ã¹ã§ã¯ãªãï¼ã 1390 move = '<span class="plog-move"><span class="plog-new">' + plogYen(c.new_price) + '</span></span>'; 1391 tag = '<span class="plog-tag new" title="ãã®åºã«æ°ããæ²è¼ããã¾ãã">NEW</span>'; 1392 } else if (c.kind === 'delisted') { 1393 move = '<span class="plog-move"><span class="plog-old">' + plogYen(c.old_price) + '</span></span>'; 1394 tag = '<span class="plog-tag gone" title="ãã®åºã®æ²è¼ãçµäºãã¾ãã">END</span>'; 1395 } else { 1396 move = '<span class="plog-move"><span class="plog-old">' + plogYen(c.old_price) + 1397 '</span> â <span class="plog-new">' + plogYen(c.new_price) + '</span></span>'; 1398 var up = c.diff > 0; 1399 tag = '<span class="plog-diff ' + (up ? 'up' : 'down') + '">' + (up ? 'â²' : 'â¼') + 1400 Number(Math.abs(c.diff)).toLocaleString('ja-JP') + '</span>'; 1401 } 1402 // ð´ grid ã®åã«è¼ããã®ã§ **4 ã¤ã¨ã .plog-row ã®ç´ä¸**ã«ç½®ãã 1403 // åºåã¨å¤åãã div ã§å ã㨠grid ã®å㯠3 ã¤ã«ãªããå¤åãã® 1404 // éå§ä½ç½®ãåºåã®é·ãã§è¡ãã¨ã«ãããï¼ï¼ç¸¦ã«èªããªãï¼ãã 1405 // åºåã® ellipsis ãå¹ããªãï¼inline è¦ç´ ã¯çç¥ãããªãï¼ã 1406 return '<div class="plog-row">' + when + store + move + tag + '</div>'; 1407 }).join(''); 1408 1409 // çµãè¾¼ã¿ä¸ã¯ãä½ä»¶ä¸ããã¤ãããåºããç·æ°ã ãã ã¨å¡ä¾ã§æ¶ããã¶ãã 1410 // æ¶ããã®ãå ããç¡ãã®ãåãããªãã 1411 var scope = _plogHidden.size 1412 ? PLOG_DAYS + 'æ¥éã§ ' + list.length + ' ä»¶ï¼å ¨ ' + _plogAll.length + ' ä»¶ä¸ã»å¡ä¾ã§çµãè¾¼ã¿ä¸ï¼' 1413 : PLOG_DAYS + 'æ¥éã§ ' + list.length + ' ä»¶ã®å¤å'; 1414 var range = totalPages > 1 ? 'ã' + (from + 1) + 'â' + to + ' ä»¶ç®' : ''; 1415 pane.innerHTML = '<div class="plog">' + rows + '</div>' + 1416 _plogPagerHtml(totalPages) + 1417 '<div class="plog-foot">' + scope + range + '</div>'; 1418 _setPlogLive(scope + (range ? 'ã' + range.trim() : '')); 1419 } 1420 1421 // ãã¼ã¸ã£ã¯ã¯ãªãã¯ã®ãã³ã«ä½ãç´ãã®ã§ããªã¹ã㯠pane ã« 1 åã ãå§è²ããã 1422 function setupPriceLogPager() { 1423 var pane = document.getElementById('logPane'); 1424 if (!pane || pane.dataset.pagerBound) return; 1425 pane.dataset.pagerBound = '1'; 1426 // ð´ ãã¤ã³èªä½ã live ã«ããªããinnerHTML ã丸ãã¨å·®ãæ¿ããã®ã§å ¨ãã¼ãã 1427 // ãæ°è¦ãæ±ãã«ãªãããã¼ã¸ãéããã³ 20 è¡ï¼ãã¼ã¸ã£ï¼ããã¿ãå ¨é¨ 1428 // èªã¿ä¸ãã¦ãã¾ããèªã¾ãããã®ã¯ãNâM ä»¶ç®ãã ããªã®ã§ãå°ç¨ã® 1429 // è¦ããªãå°é åã 1 ã¤ç½®ããããã«ã ãç¯å²ã®æååãå ¥ããã 1430 if (!document.getElementById('plogLive')) { 1431 var live = document.createElement('div'); 1432 live.id = 'plogLive'; 1433 live.className = 'sr-only'; 1434 live.setAttribute('aria-live', 'polite'); 1435 live.setAttribute('aria-atomic', 'true'); 1436 pane.parentNode.insertBefore(live, pane); 1437 } 1438 1439 function goPage(n) { 1440 if (!n || n === _plogPage) return; 1441 _plogPage = n; 1442 renderPriceLog(false); 1443 // ãã¼ã¸ãå¤ããã 1 è¡ç®ãè¦ããä½ç½®ã¸ãæ¼ããä½ç½®ãæ«å°¾ãªã®ã§ã 1444 // ãã®ã¾ã¾ã ã¨æ°ãããã¼ã¸ã®æå¾å°¾ãè¦ããã¨ã«ãªãã 1445 try { pane.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (_) { pane.scrollIntoView(); } 1446 } 1447 1448 pane.addEventListener('click', function (e) { 1449 // ð´ `.plog-pg` ã¯å ¥åæ¬ã«ãåæ¹ä¸è´ããªãããå¥ã¯ã©ã¹ã«ãã¦ãã 1450 // ï¼`.plog-pg-input` 㯠button ã§ã¯ãªãã®ã§ closest ã«ã¯æãããªãï¼ 1451 var btn = e.target.closest ? e.target.closest('button.plog-pg') : null; 1452 if (!btn || btn.disabled) return; 1453 goPage(parseInt(btn.dataset.p, 10)); 1454 }); 1455 1456 function jumpFromInput(el) { 1457 var total = parseInt(el.max, 10) || 1; 1458 var n = parseInt(el.value, 10); 1459 // 空ã»éæ°åã»ç¯å²å¤ã¯**é£ã°ãã«ç¾å¨ãã¼ã¸ã¸æ»ã**ãåæã« 1 ãã¼ã¸ç®ã¸ 1460 // é£ã°ãã¨ãæã¡ééãã§èªãã§ããå ´æã失ãã 1461 if (!n || n < 1 || n >
1461 total) { el.value = String(_plogPage); return; } 1462 if (n === _plogPage) return; 1463 goPage(n); 1464 } 1465 pane.addEventListener('keydown', function (e) { 1466 if (e.key !== 'Enter' || !e.target.classList.contains('plog-pg-input')) return; 1467 e.preventDefault(); 1468 e.target.blur(); // ã¢ãã¤ã«ã®ãã¼ãã¼ããéãã¦ããæãç´ã 1469 jumpFromInput(e.target); 1470 }); 1471 pane.addEventListener('change', function (e) { 1472 if (e.target.classList.contains('plog-pg-input')) jumpFromInput(e.target); 1473 }); 1474 } 1475 1476 // å¡ä¾ãã°ã«ããã®åæç»ãå±¥æ´ãéãã¦ããæã¯ä½ãããªãï¼éããæã«æãããï¼ 1477 function refreshPriceLogFilter(hiddenKeys) { 1478 _plogHidden = hiddenKeys || new Set(); 1479 var pane = document.getElementById('logPane'); 1480 if (pane && !pane.hidden && _plogAll.length) renderPriceLog(true); 1481 } 1482 1483 async function loadPriceLog() { 1484 var pane = document.getElementById('logPane'); 1485 if (!pane || !currentJan) return; 1486 if (_plogLoadedJan === currentJan) { 1487 // ð´ å徿¸ã¿ã§ã**æãç´ã**ãã°ã©ãå´ã«å± ãéã«å¡ä¾ã§åºãæ¶ãã¦ã 1488 // `refreshPriceLogFilter` ã¯é表示ã®ãã¤ã³ãæããªãï¼éããæã« 1489 // æãç´æï¼ã®ã§ãããã§ return ããã¨çµãè¾¼ã¿ãå¹ãã¦ããªãå¤ã 1490 // HTML ããã®ã¾ã¾åºãï¼åç¾: å±¥æ´âã°ã©ãâå¡ä¾ã§åºãæ¶ãâå±¥æ´ï¼ã 1491 // ð´ reset ã¯æ¸¡ããªããçµãè¾¼ã¿ãåæ ãç´ãããã ãã§ã 1492 // ãã¼ã¸ã¾ã§å é ã«æ»ã㨠5 ãã¼ã¸ããååã§å¾å¾©ã®ãã³èªãå ´æã失ãã 1493 // ä»¶æ°ãæ¸ã£ã¦ç¾å¨ãã¼ã¸ãæ¶ããæã¯ renderPriceLog å´ã丸ããã 1494 renderPriceLog(false); 1495 return; 1496 } 1497 // ð´ ãèªã¿è¾¼ã¿ä¸â¦ãã¯ã²ã¼ãã®**å¾**ã§ã ãæ¸ããããã«ç½®ãã¨ãã²ã¼ãã 1498 // æ¡å æã§ä¸æ¸ãããã®ã§æç»ãããªãæ»ãã æ¸ãè¾¼ã¿ã«ãªããå°æ¥ãã®éã« 1499 // await ãå ¥ã£ãç¬éã«ä¸ç¬ã ã誤ã£ãç¶æ ãè¦ããã 1500 // ð´ await ãã¾ããã®ã§ JAN ã**åºå®**ããã`currentJan` ãå¾ã§èªã¿ç´ãã¨ã 1501 // å¾ ã£ã¦ããéã«å¥ååã¸ç§»ã£ãæã«ã_plogLoadedJan 㯠Bã»ä¸èº«ã¯ Aãã« 1502 // ãªããæ©æ return ã®ããã§**äºåº¦ã¨åãç´ãããªã**ï¼åå B ã«åå A ã® 1503 // å¤åå±¥æ´ãæ°¸ä¹ ã«åºãï¼ã 1504 var reqJan = currentJan; 1505 // ð´ `/api/product/{jan}` ã dp_seen marker ãæ¸ã**å**ã« /changes ãæããªãã 1506 // #loading ã¯è¦ã被ãããªãã®ã§ãèªã¿è¾¼ã¿ä¸ã§ãåã®ååã®ã¿ããæ¼ããã 1507 // ããã§æ¼ããã㨠marker ç¡ãã§ 403 âãã»ãã·ã§ã³ãåãã¾ãããã® 1508 // å½ã¢ã¼ãã«ãåºãï¼/history ã :319 ã§åãé åºãå®ã£ã¦ããã®ã¨åãçç±ï¼ã 1509 // ð´ é»ã£ã¦ return ããªãããèªã¿è¾¼ã¿ä¸â¦ãã®ã¾ã¾åºã¾ãã¨ãåååå¾ã失æããæ 1510 // ï¼catch 㯠setPriceLogView ãå¼ã°ãªãï¼ã showProduct ãåå¾ãã¦å®äºãã 1511 // æã«å±¥æ´ã¿ããæ°¸ä¹ ã«æ»ã¬ãæ¡å ãåºãã¦åã¿ããã§ç´ããå½¢ã«ããã 1512 if (_productReadyJan !== reqJan) { 1513 var mW = 'ååã®èªã¿è¾¼ã¿ãçµãã£ã¦ãããããä¸åº¦ãå±¥æ´ããæ¼ãã¦ãã ããã'; 1514 pane.innerHTML = '<div class="plog-empty">' + mW + '</div>'; 1515 _setPlogLive(mW); 1516 // ð´ ããã« `loaded = false` ã¨æ¸ãã¦ãããããããªå¤æ°ã¯ç¡ã 1517 // ï¼product.html åç¨¿ã®æ®éª¸ï¼ã'use strict' ãªã®ã§ ReferenceError ã« 1518 // ãªããasync 颿°ã® unhandled rejection ã¨ãã¦æ¯åæãã¦ããã 1519 // ç¶æ 㯠`_plogLoadedJan` ãæã£ã¦ãã¦ããã®æç¹ã§æ¢ã« currentJan 㨠1520 // é£ãéã£ã¦ããï¼åã¿ããã§æ®éã«ããç´ããã®ã§ãªã»ããã¯è¦ããªãã 1521 return; 1522 } 1523 pane.innerHTML = '<div class="plog-empty">èªã¿è¾¼ã¿ä¸â¦</div>'; 1524 try { 1525 var data = await apiCall('/api/product/' + reqJan + '/changes?days=' + PLOG_DAYS); 1526 if (reqJan !== currentJan) return; // å¾ ã£ã¦ããéã«å¥ååã¸ç§»ã£ã 1527 if (data && data.error) throw new Error('server'); 1528 _plogLoadedJan = reqJan; 1529 _plogAll = data.changes || []; 1530 renderPriceLog(true); 1531 } catch (e) { 1532 // ð´ é»ã£ã¦ç©ºã«ããªãã空表示ã¯ãå¤åãªããã¨åºå¥ãä»ããã 1533 // å£ãã¦ããã®ã«å¥åº·ã«è¦ããï¼[[éé³ã¯å¥åº·ã¨è¦åããä»ããªã]]ï¼ã 1534 _plogLoadedJan = null; // åã¿ããã§å試è¡ã§ãã 1535 var mE = 'å±¥æ´ãèªã¿è¾¼ãã¾ããã§ãããããä¸åº¦ãå±¥æ´ããæ¼ãã¦ãã ããã'; 1536 pane.innerHTML = '<div class="plog-empty">' + mE + '</div>'; 1537 _setPlogLive(mE); 1538 } 1539 } 1540 1541 // ã°ã©ãå°ç¨ã®æä½ï¼ã»ã¼ã«å¸¯ã»æéã»ã¬ã¯ã¿ã»brushã»æéçµ±è¨ï¼ã¯å±¥æ´ã§ã¯æå³ã 1542 // ç¡ãã®ã§ä¸ç·ã«é ããcanvas ãæ®ããã¾ã¾éãã㨠Chart.js ã® resize ãèµ°ãã 1543 // ãã¾å±¥æ´ãã¥ã¼ãã©ããï¼ã°ã©ãå°ç¨ã®æç»ãæããå¤å®ã«ä½¿ãï¼ 1544 function _isPriceLogView() { 1545 var p = document.getElementById('logPane'); 1546 return !!(p && !p.hidden); 1547 } 1548 1549 function setPriceLogView(showLog) { 1550 // ð´ ããã¹ã¦é表示ãã ãã¯é ããªãï¼ããã¯å¡ä¾ã®ä¸æ¬ãã°ã«ã§ãå±¥æ´è¡¨ã® 1551 // çµãè¾¼ã¿ã«ãå¹ããå®ä¾¡ãã°ã«ã¯åºèã§ã¯ãªãã®ã§è¡¨ã«ã¯ç¡é¢ä¿ï¼é ãã 1552 // ð´ `#noHistory` ãå ¥ãå¿ããªãããã㯠`.chart
1552-container` ã®**å å¼**ãªã®ã§ 1553 // ã³ã³ãããé ãã¦ãæ®ããå±¥æ´ãåºã¦ããçä¸ã«ãå±¥æ´ãã¼ã¿ãããã¾ãããã 1554 // 並ã¶ï¼ã°ã©ãç¨ã®å±¥æ´ãç¡ãï¼ç»é²ç´å¾ã®ååã§å®éã«èµ·ããï¼ã 1555 var ids = ['chartSaleBtn', 'chartRangeSel', 'chartSaleNote', 'chartBrush', 1556 'chartPeriodStats', 'chartMsrpToggle', 'noHistory']; 1557 var chartBox = document.querySelector('.chart-section .chart-container'); 1558 var pane = document.getElementById('logPane'); 1559 var tabC = document.getElementById('tabChartView'); 1560 var tabL = document.getElementById('tabLogView'); 1561 if (!pane || !tabC || !tabL) return; 1562 if (chartBox) chartBox.hidden = showLog; 1563 pane.hidden = !showLog; 1564 ids.forEach(function (id) { 1565 var el = document.getElementById(id); 1566 if (!el) return; 1567 if (showLog) { 1568 // å ã®è¡¨ç¤ºç¶æ ãè¦ãã¦ããé ãï¼display:none ã§å§ã¾ãç©ãããï¼ 1569 if (el.dataset.plogPrev === undefined) el.dataset.plogPrev = el.style.display || ''; 1570 el.style.display = 'none'; 1571 } else if (el.dataset.plogPrev !== undefined) { 1572 el.style.display = el.dataset.plogPrev; 1573 delete el.dataset.plogPrev; 1574 } 1575 }); 1576 tabC.classList.toggle('active', !showLog); 1577 tabL.classList.toggle('active', showLog); 1578 tabC.setAttribute('aria-selected', String(!showLog)); 1579 tabL.setAttribute('aria-selected', String(showLog)); 1580 // roving tabindex: tablist å ¨ä½ã§ Tab åæ¢ã¯ 1 åãå·¦å³ãã¼ã§ç§»åï¼ARIA tabs ã®ä½æ³ï¼ 1581 tabC.tabIndex = showLog ? -1 : 0; 1582 tabL.tabIndex = showLog ? 0 : -1; 1583 // ð´ é ãã¦ããéã¯ããããããæãã¦ããªãï¼0 å¹ ã¬ã¼ãï¼ãæ»ããæã« 1584 // æãç´ããªãã¨ãå±¥æ´ä¸ã«ãã¼ããå¤ããå ´åã¯åã®ãã¼ãã®è²ãæ®ãã 1585 if (!showLog && currentHistoryData && currentHistoryData.length) { 1586 var _bw = document.getElementById('chartBrush'); 1587 if (_bw && _bw.style.display !== 'none') _drawBrushMini(currentHistoryData); 1588 } 1589 if (showLog) loadPriceLog(); 1590 } 1591 1592 function setupPriceLogTabs() { 1593 var tabC = document.getElementById('tabChartView'); 1594 var tabL = document.getElementById('tabLogView'); 1595 if (!tabC || !tabL || tabC.dataset.bound) return; 1596 tabC.dataset.bound = '1'; 1597 tabC.addEventListener('click', function () { setPriceLogView(false); }); 1598 tabL.addEventListener('click', function () { setPriceLogView(true); }); 1599 // å·¦å³/Home/End ã§åãæ¿ããç§»åå ã¸ãã©ã¼ã«ã¹ãéã 1600 [tabC, tabL].forEach(function (btn) { 1601 btn.addEventListener('keydown', function (e) { 1602 var toLog; 1603 // ð´ tab 㯠2 æãããªãã®ã§ãARIA ã®ä½æ³ï¼ç«¯ã§æãè¿ãï¼ã«å¾ã㨠1604 // å·¦å³ã©ã¡ãã®ç¢å°ã§ãããã䏿¹ãã¸è¡ãã®ãæ£ããã 1605 // åãã§å ´ååãããã¨ãå±¥æ´ã§ â ãæ¼ããæã« 1606 // `!(btn === tabC)` ã true ã«ãªã£ã¦**ãã®å ´ã«çã¾ã**ã 1607 // ð´ æ¨ªä¸¦ã³ã® tablist ãªã®ã§ ââ ã¯**åãåããªã**ï¼APG ã§ã¯ç¸¦ tablist ç¨ï¼ã 1608 // preventDefault ã¾ã§ããã¨ãã¿ãã«ãã©ã¼ã«ã¹ãããéã¯ç¢å°ãã¼ã§ 1609 // ãã¼ã¸ãã¹ã¯ãã¼ã«ã§ããªããªãã 1610 if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') toLog = (btn === tabC); 1611 else if (e.key === 'Home') toLog = false; // å é ï¼ã°ã©ã 1612 else if (e.key === 'End') toLog = true; // æ«å°¾ï¼å±¥æ´ 1613 else return; 1614 e.preventDefault(); 1615 setPriceLogView(toLog); 1616 (toLog ? tabL : tabC).focus(); 1617 }); 1618 }); 1619 } 1620 1621 function renderChart(history, days, range) { 1622 if (typeof Chart === 'undefined') { 1623 console.error("Chart.js æªå è¼"); 1624 // ä¼é é级å¤ç 1625 return; 1626 } 1627 const canvas = document.getElementById('priceChart'); 1628 const ctx = canvas.getContext('2d'); 1629 let recentHistory; 1630 if (range) { 1631 recentHistory = history.slice(range.start, range.end + 1); 1632 } else { 1633 recentHistory = history.slice(-days); 1634 // æéãã¿ã³çµç±: ãã©ã·çªãæ«å°¾ N æ¥ã¸ãªã»ãã 1635 _brushState = { len: history.length, start: Math.max(0, history.length - recentHistory.length), end: history.length - 1 }; 1636 } 1637 renderBrush(history); 1638 1639 if (recentHistory.length === 0) { 1640 elements.noHistory.style.display = 'flex'; 1641 canvas.style.display = 'none'; 1642 if (priceChart) { priceChart.destroy(); priceChart = null; } 1643 const legendSection = document.getElementById('chartLegendSection'); 1644 if (legendSection) legendSection.style.display = 'none'; 1645 const statsEl = document.getElementById('chartPer
1645iodStats'); 1646 if (statsEl) statsEl.style.display = 'none'; 1647 return; 1648 } 1649 1650 elements.noHistory.style.display = 'none'; 1651 canvas.style.display = 'block'; 1652 1653 const storeKeys = new Set(); 1654 recentHistory.forEach(day => { 1655 Object.keys(day.prices || {}).forEach(key => storeKeys.add(key)); 1656 }); 1657 1658 const storeNames = KX_STORE_NAME_BY_KEY; 1659 1660 // è²·å䏿ï¼ä¾¡æ ¼0/æ¬ æ¸¬ï¼åºéã¯ç ´ç·ã§æ©æ¸¡ãï¼2026-07-25 May è£æ±º: 1661 // æç·ã§ã¯ãªãèç·æ¥ç¶ââ䏿ãè¦ã¦åããã¤ã¤æµãã追ããï¼ 1662 const _gapDash = (ctx) => (ctx.p0.skip || ctx.p1.skip) ? [5, 5] : undefined; 1663 const datasets = Array.from(storeKeys).map(key => ({ 1664 label: storeNames[key] || key, 1665 data: recentHistory.map(day => day.prices?.[key] || null), 1666 borderColor: STORE_COLORS[key] || '#94A3B8', 1667 backgroundColor: (STORE_COLORS[key] || '#94A3B8') + '20', 1668 // ç¹åã¯é表示ï¼20åºÃ90æ¥ã§è¦è¦ãã¤ãºã«ãªãããï¼ãhover æã®ã¿ç¹ãåºã 1669 tension: 0.3, fill: false, pointRadius: 0, pointHoverRadius: 5, pointHitRadius: 8, 1670 spanGaps: true, segment: { borderDash: _gapDash } 1671 })); 1672 1673 // å®ä¾¡ç¹ç·ï¼iPhone/主è¦ã²ã¼ã æ©ã®ã¿ï¼ãæ¹å®æ¥ãããã°é段ç¶ã«å¤åããã 1674 const _msrpLine = recentHistory.map(day => msrpAt(day.date_iso || '')); 1675 if (_msrpLine.some(v => v != null)) { 1676 const _isDarkPre = document.documentElement.getAttribute('data-theme') === 'dark'; 1677 datasets.push({ 1678 label: 'å®ä¾¡', 1679 data: _msrpLine, 1680 borderColor: _isDarkPre ? 'rgba(148,163,184,0.85)' : '#94A3B8', 1681 backgroundColor: 'transparent', 1682 borderDash: [6, 4], borderWidth: 1.5, stepped: true, 1683 tension: 0, fill: false, pointRadius: 0, pointHoverRadius: 0, spanGaps: true 1684 }); 1685 } 1686 1687 // ä¿åå½åéèç dataset 1688 const hiddenLabels = new Set(); 1689 if (priceChart) { 1690 priceChart.data.datasets.forEach((ds, i) => { 1691 if (priceChart.getDatasetMeta(i).hidden) hiddenLabels.add(ds.label); 1692 }); 1693 priceChart.destroy(); 1694 } 1695 1696 // æ¢å¤éèç¶æ 1697 datasets.forEach(ds => { 1698 if (hiddenLabels.has(ds.label)) ds.hidden = true; 1699 }); 1700 1701 const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 1702 const gridColor = isDark ? 'rgba(255,255,255,0.08)' : '#E2E8F0'; 1703 const tickColor = isDark ? '#64748B' : '#64748B'; 1704 const legendColor = isDark ? '#94A3B8' : '#64748B'; 1705 const tooltipBg = isDark ? 'rgba(15,23,42,0.95)' : 'rgba(255,255,255,0.95)'; 1706 const tooltipFg = isDark ? '#E2E8F0' : '#1E293B'; 1707 const tooltipBorder = isDark ? '#334155' : '#E2E8F0'; 1708 1709 ensureSalePeriods(); 1710 priceChart = new Chart(ctx, { 1711 type: 'line', 1712 plugins: [_saleBandPlugin, _singlePointStubPlugin], 1713 data: { labels: recentHistory.map(day => day.date), datasets: datasets }, 1714 options: { 1715 responsive: true, maintainAspectRatio: false, 1716 interaction: { intersect: false, mode: 'index' }, 1717 plugins: { 1718 legend: { display: false }, 1719 // Canvas æç» tooltip ãç¡å¹åã HTML å¤é¨ tooltip ã§å¤åºèå¯¾å¿ 1720 tooltip: { 1721 enabled: false, 1722 external: chartTooltipExternal, 1723 itemSort: (a, b) => (b.parsed.y || 0) - (a.parsed.y || 0) 1724 } 1725 }, 1726 scales: { 1727 x: { grid: { color: gridColor }, ticks: { color: tickColor, padding: 14 } }, // padding: ã»ã¼ã«ãã¼ã«ã¼ï¼â²ï¼ã®è¡¨ç¤ºã¹ãã¼ã¹ 1728 y: { grid: { color: gridColor }, ticks: { color: tickColor, callback: value =>
1728 'Â¥' + value.toLocaleString() } } 1729 } 1730 } 1731 }); 1732 1733 // 表示ä¸åºè key ã® Set ã確å®ï¼labelâkey éå¼ãã¯ä¸å®å®ãªã®ã§ key åºæºã§æ§ç¯ï¼ 1734 // datasets 㯠Array.from(storeKeys).map(key => ({ label: storeNames[key]||key, ... })) 1735 // ã§ä½ããã¦ãããããindex ã storeKeys ã®é åºã¨ä¸è´ããã 1736 // hiddenLabels ã«å«ã¾ãã label ãæã¤ dataset ã® key ãé¤å¤ã㦠Set ãä½ãã 1737 _currentRecentHistory = recentHistory; 1738 _visibleStoreKeys = new Set( 1739 Array.from(storeKeys).filter((key, i) => !hiddenLabels.has(storeNames[key] || key)) 1740 ); 1741 1742 _bindChartTooltipDismiss(); 1743 renderChartLegend(); 1744 renderChartStats(recentHistory, _visibleStoreKeys); 1745 // å¡ä¾ã®é ãç¶æ ã¯åæç»ãã¾ããã§å¼ãç¶ãããï¼hiddenLabelsï¼ã®ã§ã 1746 // 表å´ã®çµãè¾¼ã¿ãããã§åãå å®¹ã«æãã 1747 refreshPriceLogFilter(new Set( 1748 Array.from(storeKeys).filter(function (key) { return hiddenLabels.has(storeNames[key] || key); }) 1749 )); 1750 _watchChartVisibility(); 1751 } 1752 1753 // ==================== æéçµ±è¨æ¬ï¼æé«ã»æä½ã»å¹³åï¼ ==================== 1754 // visibleKeys: 表示ä¸ã®åºè key ã® Setãnull ã®å ´åã¯å ¨åºèã対象ã¨ããï¼ãã©ã¼ã«ããã¯ï¼ã 1755 function renderChartStats(recentHistory, visibleKeys) { 1756 const el = document.getElementById('chartPeriodStats'); 1757 const elHigh = document.getElementById('cpsHigh'); 1758 const elHighDate = document.getElementById('cpsHighDate'); 1759 const elLow = document.getElementById('cpsLow'); 1760 const elLowDate = document.getElementById('cpsLowDate'); 1761 const elAvg = document.getElementById('cpsAvg'); 1762 if (!el || !elHigh || !elLow || !elAvg) return; 1763 1764 // å ¨ãã¤ã³ããåéï¼æ¥ä»ã¨ä¾¡æ ¼ã®ãã¢ã¨ãã¦ä¿æï¼ 1765 // visibleKeys ãæå®ããã¦ããã°ããã® key ã«å«ã¾ããåºèã ããéè¨ããã 1766 // åãæé«å¤ / æä½å¤ãè¤æ°æ¥ã«ããã¨ããæãæ©ãï¼æå¤ï¼æ¥ä»ããæ¡ç¨ãããã 1767 // æé ï¼å¤ãæ¥ããæ°ããæ¥ï¼ã§ã¹ãã£ã³ããæåã«ããããããã®ãè¨é²ããã 1768 let maxVal = null, maxDate = ''; 1769 let minVal = null, minDate = ''; 1770 let sum = 0, count = 0; 1771
1772 recentHistory.forEach(day => { 1773 const dayDate = day.date_iso || day.date || ''; 1774 Object.entries(day.prices || {}).forEach(([key, price]) => { 1775 // é表示ã®åºè key ã¯éè¨ããé¤å¤ãã 1776 if (visibleKeys && !visibleKeys.has(key)) return; 1777 if (!price || price <= 0) return; 1778 sum += price; 1779 count++; 1780 // æé«: ç¾å¨ã® maxVal ããçã«å¤§ããã¨ãã ãæ´æ°ï¼åå¤ã¯æåã®æ¥ãä¿æ = æå¤ï¼ 1781 if (maxVal === null || price > maxVal) { 1782 maxVal = price; 1783 maxDate = dayDate; 1784 } 1785 // æä½: åæ§ 1786 if (minVal === null || price < minVal) { 1787 minVal = price; 1788 minDate = dayDate; 1789 } 1790 }); 1791 }); 1792 1793 if (count === 0) { 1794 el.style.display = 'none'; 1795 return; 1796 } 1797 1798 const avg = Math.round(sum / count); 1799 elHigh.textContent = 'Â¥' + maxVal.toLocaleString(); 1800 elHighDate.textContent = maxDate || ''; 1801 elLow.textContent = 'Â¥' + minVal.toLocaleString(); 1802 elLowDate.textContent = minDate || ''; 1803 elAvg.textContent = 'Â¥' + avg.toLocaleString(); 1804 1805 // å®ä¾¡ã»ã«: ç¾è¡å®ä¾¡ + 表示æéå ã®æ¹å®ãã°ï¼ä¾: Â¥194,800 â 07/18æ¹å®ï¼ 1806 const elMsrp = document.getElementById('cpsMsrp'); 1807 if (elMsrp) { 1808 const elMV = document.getElementById('cpsMsrpValue'); 1809 const elMN = document.getElementById('cpsMsrpNote'); 1810 const firstIso = recentHistory[0]?.date_iso || ''; 1811 const lastIso = recentHistory[recentHistory.length - 1]?.date_iso || ''; 1812 const cur = msrpAt(lastIso); 1813 if (cur && elMV && elMN) { 1814 elMV.textContent = 'Â¥' + cur.toLocaleString(); 1815 const changes = (currentMsrpHistory || []).filter(e => e.from > firstIso && e.from <= lastIso); 1816 if (changes.length) { 1817 const lastChg = changes[changes.length - 1]; 1818 const idx = currentMsrpHistory.indexOf(lastChg); 1819 const prev = idx > 0 ? currentMsrpHistory[idx - 1].msrp : null; 1820 const md = lastChg.from.slice(5).replace('-', '/'); 1821 elMN.textContent = (prev ? `Â¥${prev.toLocaleString()} â ` : '') + `${md}æ¹å®` 1822 + (changes.length > 1 ? `ï¼æéå ${changes.length}åï¼` : ''); 1823 elMsrp.title = changes.map(c => { 1824 const i2 = currentMsrpHistory.indexOf(c); 1825 const p2 = i2 > 0 ? currentMsrpHistory[i2 - 1].msrp : null; 1826 return `${c.from}: ${p2 ? 'Â¥' + p2.toLocaleString() + ' â ' : ''}Â¥${c.msrp.toLocaleString()}`; 1827 }).join('\n'); 1828 } else { 1829 elMN.textContent = ''; 1830 elMsrp.title = 'å®ä¾¡ï¼ç¨è¾¼ï¼ã»è¡¨ç¤ºæéå ã®æ¹å®ãªã'; 1831 } 1832 elMsrp.style.display = ''; 1833 } else { 1834 elMsrp.style.display = 'none'; 1835 } 1836 } 1837 el.style.display = ''; 1838 } 1839 1840 // å¡ä¾ãã°ã«å¾ã«çµ±è¨ãåè¨ç®ããã_visibleStoreKeys ã Chart.js ã®ç¾ç¶ãã 1841 // åæ§ç¯ããããã§ renderChartStats ãå¼ã¶ã 1842 function _recomputeChartStats() { 1843 if (!priceChart || !_currentRecentHistory) return; 1844 const storeNames = KX_STORE_NAME_BY_KEY; 1845 // ç¾å¨ priceChart ã«ç»é²ããã¦ãã dataset ã® hidden ç¶æ ããè¡¨ç¤ºä¸ key Set ãåæ§ç¯ã 1846 // dataset.label = storeNames[key] || key ã§ä½ããã¦ããã®ã§ã 1847 // KX_STORE_NAME_BY_KEY ã®éå¼ã Map ã使ã£ã¦ key ã«æ»ãã 1848 const labelToKey = {};
1849 Object.entries(storeNames).forEach(([k, v]) => { labelToKey[v] = k; }); 1850 const keys = new Set(); 1851 priceChart.data.datasets.forEach((ds, i) => { 1852 if (ds.label === 'å®ä¾¡') return; // å®ä¾¡ç¹ç·ã¯åºèã§ã¯ãªãã®ã§çµ±è¨å¯¾è±¡å¤ 1853 if (priceChart.getDatasetMeta(i).hidden) return; 1854 // label ã storeNames ã«åå¨ããå ´å㯠key ã«æ»ãã 1855 // storeNames ã«åå¨ããªãå ´å㯠key ãã®ãã®ã label ãªã®ã§ç´æ¥ä½¿ãã 1856 const key = labelToKey[ds.label] || ds.label; 1857 keys.add(key); 1858 }); 1859 _visibleStoreKeys = keys; 1860 renderChartStats(_currentRecentHistory, _visibleStoreKeys); 1861 // ð´ `renderChartStats` ã¯æ«å°¾ã§ #chartPeriodStats ã« display='' ã**ç¡æ¡ä»¶ã§** 1862 // æ¸ããå¡ä¾ãããã¨ããã¹ã¦é表示ãã¯å±¥æ´ãã¥ã¼ã§ãçããã¦ããï¼è¡¨ã® 1863 // çµãè¾¼ã¿ææ®µã ããï¼ã®ã§ãå±¥æ´ã®ã¾ã¾æ¼ãã¨ã°ã©ãå°ç¨ã®æéçµ±è¨ã 1864 // 表ã®ä¸ã«çãã¦ãããplogPrev ã¯æ¢ã«ç«ã£ã¦ããã®ã§ setPriceLogView 㯠1865 // ããé ãç´ãã¦ãããªãï¼ã¿ããæ¼ãç´ãã¾ã§æ®ããããã§éãã¦ããã 1866 if (!_isPriceLogView()) { /* ã°ã©ã表示ä¸ã¯ãã®ã¾ã¾ */ } 1867 else { var _cps = document.getElementById('chartPeriodStats'); 1868 if (_cps) _cps.style.display = 'none'; } 1869 // å¡ä¾ã¯ã°ã©ãã ãã§ãªã**å±¥æ´è¡¨ã**çµãï¼May æç¤º 2026-09-05ï¼ã 1870 // ð´ ã表示ä¸ã«ç¡ãï¼é ããã§ã¯ãªããã°ã©ãã®å ¨ key â è¡¨ç¤ºä¸ keyããé ãã 1871 // å±¥æ´ã® 14 æ¥çªã«ã¯ã°ã©ãã®æéã«ç¹ãç¡ãåºï¼ä¸æ¶æ¸ã¿çï¼ãå± ãã®ã§ã 1872 // åè ã ã¨è§¦ã£ã¦ãããªãåºã®è¡ãåæã«æ¶ããã 1873 var _allKeys = new Set(); 1874 priceChart.data.datasets.forEach(function (ds) { 1875 if (ds.label === 'å®ä¾¡') return; 1876 _allKeys.add(labelToKey[ds.label] || ds.label); 1877 }); 1878 var _hidden = new Set(); 1879 _allKeys.forEach(function (k) { if (!keys.has(k)) _hidden.add(k); }); 1880 refreshPriceLogFilter(_hidden); 1881 } 1882
1883 // iOS Safari ã¯ã¡ã¢ãªå§ã§ç»é¢å¤ canvas ã®æç»ãããã¡ãç ´æ£ãããã¨ãããã 1884 // ã¹ã¯ãã¼ã«ã§å°éããæã«çã£ç½ãªãã£ã¼ãã«ãªãããã¥ã¼ãã¼ãé²å ¥æã« 1885 // åæç»ãã¦å¾©å ããï¼canvas è¦ç´ ã¯åºå®ãªã®ã§ observe 㯠1 åã ãï¼ã 1886 let _chartRedrawIO = null; 1887 function _watchChartVisibility() { 1888 if (_chartRedrawIO || !('IntersectionObserver' in window)) return; 1889 const canvas = document.getElementById('priceChart'); 1890 if (!canvas) return; 1891 _chartRedrawIO = new IntersectionObserver(function (entries) { 1892 entries.forEach(function (e) { 1893 if (e.isIntersecting && priceChart) { 1894 try { priceChart.draw(); } catch (err) {} 1895 } 1896 }); 1897 }); 1898 _chartRedrawIO.observe(canvas); 1899 } 1900 // bfcache å¾©å¸°ï¼æ»ã/é²ãï¼ã§ã canvas ã空ã«ãªããã¨ãããããåæç» 1901 window.addEventListener('pageshow', function (e) { 1902 if (e.persisted && priceChart) { 1903 try { priceChart.update('none'); } catch (err) {} 1904 } 1905 }); 1906 1907 // ==================== ã«ã¹ã¿ã HTML å¡ä¾ ==================== 1908 // ==================== ãã¼ã¯/ã©ã¤ãåæ¿æã®ãã£ã¼ãåé è² ==================== 1909 // Chart.js ã¯çææã«è²ã確å®ããããããã¼ãåæ¿å¾ãæ§ãã¼ãã® 1910 // ã°ãªãã/ç®çãè²ãæ®ããdata-theme ã®å¤åãç£è¦ãã¦å³ææ´æ°ããã 1911 new MutationObserver(function () { 1912 if (!priceChart) return; 1913 try { 1914 const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 1915 const gridColor = isDark ? 'rgba(255,255,255,0.08)' : '#E2E8F0'; 1916 const tickColor = '#64748B'; 1917 const sx = priceChart.options.scales.x, sy = priceChart.options.scales.y; 1918 sx.grid.color = gridColor; sy.grid.color = gridColor; 1919 sx.ticks.color = tickColor; sy.ticks.color = tickColor; 1920 priceChart.update('none'); 1921 if (typeof currentHistoryData !== 'undefined' && currentHistoryData) _drawBrushMini(currentHistoryData); 1922 } catch (e) {} 1923 }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); 1924 1925 (function bindSaleToggle() { 1926 document.addEventListener('click', function(e) { 1927 if (e.target && e.target.id === 'chartSaleBtn') { 1928 _showSaleBands = !_showSaleBands; 1929 try { localStorage.setItem('kx_chart_sale', _showSaleBands ? '1' : '0'); } catch (err) {} 1930 _syncSaleBtn(); 1931 if (priceChart) priceChart.update(); 1932 return; 1933 } 1934 // å¡ä¾ããã: ãã©ãããã©ã¼ã å¥ã®å¸¯ãã£ã«ã¿ã¼ 1935 const typeBtn = e.target && e.target.closest ? e.target.closest('#saleTypeAmazon, #saleTypeRakuten') : null; 1936 if (!typeBtn) return; 1937 const key = typeBtn.id === 'saleTypeRakuten' ? 'rakuten' : 'amazon'; 1938 _saleTypes[key] = !_saleTypes[key]; 1939 try { 1940 localStorage.setItem('kx_chart_sale_types', 1941 ['amazon', 'rakuten'].filter(k => _saleTypes[k]).join(',')); 1942 } catch (err) {} 1943 _syncSaleBtn(); 1944 if (priceChart) priceChart.update(); 1945 }); 1946 document.addEventListener('DOMContentLoaded', _syncSaleBtn); 1947 })(); 1948 1949 function renderChartLegend() { 1950 const section = document.getElementById('chartLegendSection'); 1951 const items = document.getElementById('chartLegendItems'); 1952 const toggleBtn = document.getElementById('chartLegendToggleAll'); 1953 if (!section || !items || !toggleBtn || !priceChart) return; 1954 1955 const datasets = priceChart.data.datasets || []; 1956 if (datasets.length === 0) { section.style.display = 'none'; return; } 1957 section.style.display = 'block'; 1958 1959 // å®ä¾¡ã¯ãããã«ãããå³ä¸ã®ç¬ç«ãã°ã«ãã¿ã³ï¼#chartMsrpToggleï¼ã§æ±ã 1960 const _msrpIdx = datasets.findIndex(ds => ds.label === 'å®ä¾¡'); 1961 items.innerHTML = datasets.map((ds, i) => { 1962 if (i === _msrpIdx) return ''; 1963 const hidden = priceChart.getDatasetMeta(i).hidden; 1964 const color = ds.borderColor || '#94A3B8'; 1965 const labelText = ds.label || ''; 1966 const safeLabel = labelText.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); 1967 return `<span class="chart-legend-item${hidden ? ' is-hidden' : ''}" data-idx="${i}">` 1968 + `<span class="chart-legend-dot" style="background:${color}"></span>${safeLabel}</span>`;
1969 }).join(''); 1970 1971 // å®ä¾¡ãã°ã«ãã¿ã³ï¼å®ä¾¡ãã¼ã¿ãããååã®ã¿è¡¨ç¤ºï¼ 1972 const msrpBtn = document.getElementById('chartMsrpToggle'); 1973 if (msrpBtn) { 1974 if (_msrpIdx >= 0) { 1975 msrpBtn.style.display = ''; 1976 msrpBtn.classList.toggle('is-hidden', !!priceChart.getDatasetMeta(_msrpIdx).hidden); 1977 msrpBtn.onclick = () => { 1978 const meta = priceChart.getDatasetMeta(_msrpIdx); 1979 meta.hidden = !meta.hidden; 1980 priceChart.update(); 1981 msrpBtn.classList.toggle('is-hidden', meta.hidden); 1982 }; 1983 } else { 1984 msrpBtn.style.display = 'none'; 1985 } 1986 } 1987 1988 items.querySelectorAll('.chart-legend-item').forEach(el => { 1989 el.addEventListener('click', () => { 1990 const idx = parseInt(el.dataset.idx, 10); 1991 const meta = priceChart.getDatasetMeta(idx); 1992 meta.hidden = !meta.hidden; 1993 priceChart.update(); 1994 el.classList.toggle('is-hidden', meta.hidden); 1995 updateLegendToggleBtn(); 1996 _recomputeChartStats(); // å¡ä¾ãã°ã« â çµ±è¨åè¨ç® 1997 }); 1998 }); 1999 2000 toggleBtn.onclick = () => { 2001 // å®ä¾¡ã¯åºèã§ã¯ãªãã®ã§ããã¹ã¦é表示/表示ãã®å¯¾è±¡å¤ï¼ç¬ç«ãã¿ã³ã§å¶å¾¡ï¼ 2002 const storeIdx = datasets.map((ds, i) => i).filter(i => datasets[i].label !== 'å®ä¾¡'); 2003 const allHidden = storeIdx.every(i => priceChart.getDatasetMeta(i).hidden); 2004 const target = !allHidden; 2005 storeIdx.forEach(i => { priceChart.getDatasetMeta(i).hidden = target; }); 2006 priceChart.update(); 2007 items.querySelectorAll('.chart-legend-item').forEach(el => el.classList.toggle('is-hidden', target)); 2008 updateLegendToggleBtn(); 2009 _recomputeChartStats(); // ããã¹ã¦é表示/表示ãâ çµ±è¨åè¨ç® 2010 }; 2011 2012 updateLegendToggleBtn(); 2013 } 2014 2015 function updateLegendToggleBtn() { 2016 const btn = document.getElementById('chartLegendToggleAll'); 2017 if (!btn || !priceChart) return; 2018 const datasets = priceChart.data.datasets || []; 2019 const storeIdx = datasets.map((ds, i) => i).filter(i => datasets[i].label !== 'å®ä¾¡'); 2020 const allHidden = storeIdx.length > 0 && storeIdx.every(i => priceChart.getDatasetMeta(i).hidden); 2021 btn.textContent = allHidden ? 'ãã¹ã¦è¡¨ç¤º' : 'ãã¹ã¦é表示'; 2022 } 2023 2024 // ==================== UI è¾ å©å½æ° ==================== 2025 function showLoading(show) { elements.loading.style.display = show ? 'flex' : 'none'; } 2026 2027 function showError(message) { 2028 elements.searchError.innerHTML = ` 2029 <div style="background: var(--danger); color: white; padding: 16px; border-radius: 12px; margin-top: 16px;"> 2030 â ï¸ ${escapeHtml(message)} 2031 </div> 2032 `; 2033 } 2034 function hideError() { elements.searchError.innerHTML = ''; } 2035 2036 const _backConfig = { 2037 'ranking': { text: 'ã©ã³ãã³ã°ã«æ»ã', url: null }, 2038 'brand': { text: 'ãã©ã³ããã¼ã¸ã«æ»ã', url: null }, 2039 'category': { text: 'ã«ãã´ãªãã¼ã¸ã«æ»ã', url: null }, 2040 'store': { text: 'è²·ååºãã¼ã¸ã«æ»ã', url: null }, 2041 'products': { text: 'å ¨ååä¸è¦§ã«æ»ã', url: '/products' }, 2042 'search': { text: 'æ¤ç´¢çµæã«æ»ã', url: null }, 2043 'profit': { text: 'å©çã©ã³ãã³ã°ã«æ»ã', url: '/ranking/profit' }, 2044 'related': { text: 'ååãã¼ã¸ã«æ»ã', url: null }, 2045 'favorites': { text: 'ãæ°ã«å ¥ãã«æ»ã', url: '/favorites' }, 2046 'follows': { text: 'é ä¿¡éç¥ã«æ»ã', url: '/favorites?tab=notify' }, 2047 'popular': { text: 'ãããã«æ»ã', url: '/' }, 2048 'history': { text: 'ãããã«æ»ã', url: '/' }, 2049 'series': { text: 'ã·ãªã¼ãºãã¼ã¸ã«æ»ã', url: null }, 2050 'article': { text: 'ã³ã©ã ã«æ»ã', url: null }, 2051 'articles': { text: 'è²·åã³ã©ã ä¸è¦§ã«æ»ã', url: '/articles' }, 2052 'apple-stock': { text: 'Appleå¨åº«ãã§ãã¯ã«æ»ã', url: null }, 2053 'statistics': { text: 'å¸å ´çµ±è¨ã«æ»ã', url: null }, 2054 'guide': { text: 'ä½¿ãæ¹ã¬ã¤ãã«æ»ã', url: '/guide' }, 2055 'deals': { text: 'ãè²·ãå¾ååã«æ»ã', url: null }, 2056 }; 2057 2058 function _updateBackBtn(from) { 2059 const cfg = _backConfig[from] || { text: 'ãããã«æ»ã', url: '/' }; 2060 const btn = elements.backBtn; 2061 // preserve SVG, replace the label. æå㯠.back-btn-label ã«å ¥ããï¼çãç»é¢ï¼â¤480pxï¼ã§ã¯ 2062 // ç¢å°ã ããè¦ãã¦æåã¯èªã¿ä¸ãç¨ã«æ®ãï¼kaitorix.cssãé·ããå©çã©ã³ãã³ã°ã«æ»ããçã§ 2063 // åãè¡ã®æ¤ç´¢æ¬ã¨ã¹ãã£ã³ãã¿ã³ãç»é¢å¤ã¸æ¼ãåºããã¦ããã2026-09-27ï¼ 2064 const svg = btn.querySelector('svg'); 2065 btn.textContent = ''; 2066 if (svg) btn.appendChild(svg); 2067 const label = document.createElement('span'); 2068 label.className = 'back-btn-label'; 2069 label.textContent = cfg.text; 2070 btn.appendChild(label); 2071 } 2072 2073 function goBack(skipPush) { 2074 // from ãã©ã¡ã¼ã¿ãããã°ãã®ç»é¢ã«æ»ã 2075 const cfg = _backConfig[currentFrom]; 2076 if (cfg) { 2077 if (cfg.url) { 2078 window.location.href = cfg.url + (currentFromQuery ? (cfg.url.includes('?') ? '&' : '?') + currentFromQuery : ''); 2079 } else if (currentFromUrl) {
2080 // from_url ãä¿åããã¦ããã°ç´æ¥ãã®ãã¼ã¸ã¸ï¼ranking/brand/category/store/search ãªã©ï¼ 2081 window.location.href = currentFromUrl; 2082 } else { 2083 // ãã©ã¦ã¶å±¥æ´ã§æ»ãï¼ãã©ã¼ã«ããã¯ï¼ 2084 history.back(); 2085 } 2086 return; 2087 } 2088 elements.productSection.classList.remove('active'); 2089 elements.searchSection.style.display = 'block'; 2090 elements.searchResults.innerHTML = ''; 2091 hideDropdown(); 2092 currentJan = null; 2093 currentProductData = null; 2094 // ååãã¼ã¸åå â ãããã«æ»ã£ãå ´åã人æ°/å©çåå/æè¿è¦ã ãæªãã¼ãã®ã㨠2095 // ããããããããã§ï¼åªçã«ï¼ãã¼ããããå±¥æ´ã¯å¥é常ã«ãã¼ãæ¸ã¿ã 2096 ensureHomeSectionsLoaded(); 2097 updateMatomeUI(); 2098 if (!skipPush) history.pushState(null, '', '/'); 2099 trackPageView('/'); 2100 // Reset meta to default when leaving product page 2101 document.title = 'è²·åX | è²·åæ¯è¼ãµã¤ã - è¤æ°ç¤¾ã®è²·åä¾¡æ ¼ã䏿¬æ¯è¼ã»è²·åãã§ãã«ã¼'; 2102 } 2103 2104 // è²·åãã§ãã«ã¼æ²è¼åãªãã該å½ãã§ãã«ã¼ã¸ã®å°ç·ãååãã¼ã¸ã«åºãã 2105 // ãã¤ãã¬ã¼ãã®ããã«ã·ãªã¼ãºé ãã«ãã´ãªé ãç¡ãã¸ã£ã³ã«ã§ã¯ããããå¯ä¸ã®å é¨ãªã³ã¯ã 2106 // SPA shell 㯠/ 㨠/dp ã§å ±ç¨ã® 1 æãªã®ã§ãµã¼ãå´ã§å·®ãè¾¼ããªãã 2107 // ãããã¯èµ·åæã«ãµã¼ãã window.KX_CHECKER_JANS ã¸åãè¾¼ãã 2108 function renderCheckerBanner(jan) { 2109 const box = document.getElementById('checkerBannerContainer'); 2110 if (!box) return; 2111 const key = (window.KX_CHECKER_JANS || {})[String(jan || '')]; 2112 const conf = key && (window.KX_CHECKERS || {})[key]; 2113 2114 // Apple 製åã¯ãè²·åãã§ãã«ã¼ / ãã®ä¸ä»£ / Apple Store å¨åº«ç¢ºèªãã 2115 // 1 æã®ã«ã¼ãã«ã¾ã¨ãã¦åºããæè¨ãã¢ã¤ã³ã³ããµã¼ã宿°ããã®ã¾ã¾åãåã 2116 // ï¼ããã«æ¸ãåã㨠SSR ã¨å端ã§å¥ã ã®æé¢ã«ãªããå®éã«ãããªã£ã¦ããï¼ã 2117 const T = window.KX_APPLE_TEXT, IC = window.KX_APPLE_ICONS || {}; 2118 const AS = window.KX_APPLE_SERIES || {}; 2119 const janKey = String(jan || ''); 2120 const series = AS[janKey]; // ç¡ãä¸ä»£ã¯ç©ºæå 2121 const isApple = !!T && Object.prototype.hasOwnProperty.call(AS, janKey); 2122 if (isApple && T) { 2123 // çåã®ã»ã°ã¡ã³ã 1 æ¬ï¼ã¢ã¤ã³ã³ï¼ååï¼ã説ææãç¢å°ãä»ããªãã 2124 // æè¨ã»ã¢ã¤ã³ã³ã¯ãµã¼ã宿°ããã®ã¾ã¾ä½¿ãï¼åç«¯ã«æ¸ãåããªãï¼ã 2125 const chip = (url, t, icon, tone) => ` 2126 <a class="kx-ckc kx-ckc-${tone}" href="${escapeHtml(url)}"><svg class="kx-ckc-ico" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon || ''}</svg><span>${escapeHtml(t)}</span></a>`; 2127 let items = ''; 2128 if (conf) items += chip(conf.url, T.matrix.t, IC.grid, 'm'); 2129 if (series) items += chip(series, T.series.t, IC.list, 's'); 2130 items += chip(T.stock.url, T.stock.t, IC.store, 'a'); 2131 box.innerHTML = `<nav class="kx-ckg" aria-label="${escapeHtml(T.group.h)}">${items}</nav>`; 2132 return; 2133 } 2134 2135 if (!conf) { box.innerHTML = ''; return; } 2136 box.innerHTML = ` 2137 <a class="kx-ckb" href="${escapeHtml(conf.url)}" aria-label="${escapeHtml(conf.label)}ãéã"> 2138 <span class="kx-ckb-ico" aria-hidden="true"> 2139 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 2140 <rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18"/> 2141 </svg> 2142 </span>
2143 <span class="kx-ckb-txt"> 2144 <span class="kx-ckb-t">${escapeHtml(conf.label)}</span> 2145 <span class="kx-ckb-d">${escapeHtml(conf.desc)}</span> 2146 </span> 2147 <svg class="kx-ckb-arw" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg> 2148 </a>`; 2149 } 2150 2151 2152 // ===== è²·ååºãªã³ã¯ã® Referrer ããªã·ã¼ ===== 2153 // æ¢å®ã¯ unsafe-urlï¼å®å ¨ URL ãéãï¼ãåºå´ã® GA ã« 2154 // https://kaitorix.app/product/{JAN} ãæ®ããææºäº¤æ¸ã®å®ç¸¾ææã«ãªãã 2155 // ä¾å¤ï¼Referer ã« "kaitorix.app" ãå ¥ã㨠403 ã§å¼¾ãåºï¼origin nginx ã® 2156 // é¨åä¸è´è¦åãreferrerpolicy="origin" ã§ããã¡ã¤ã³åã¯è¼ãã®ã§ç¡å¹ï¼ 2157 // no-referrer 以å¤ã«æãç¡ãï¼ãè½ã¡ã¦ãéã㪠403 ã§ãã°ã«ä½ãåºãªãããã 2158 // scripts/referer_audit.py ã鱿¬¡ã§å®æ¸¬ããã 2159 // ð´ ãã®é å㯠referer_audit.py ãèªååæãããè¡ã®å½¢ãå¤ããªããã¨ã 2160 const KX_REFERRER_BLOCKED_HOSTS = ['newenoking-kaitori.com']; /* referer-audit:auto */ 2161 // ä¾¡æ ¼ãåããªããªã£ãåºï¼ãµã¼ãã¼ã® link_only_storesï¼ããä¾¡æ ¼è¡¨ã®æ«å°¾ã«ä»ã®åæ¢åºã¨åãç°è²ã®è¡ã§ä¸¦ã¹ã 2162 // ä¾¡æ ¼ã®æ¬ããåºèãµã¤ãã§ç¢ºèªãã«ããï¼2026-09-30 May æç¤ºï¼ãæ§ä¾¡æ ¼ã¯åºããªãï¼æé«å¤ã»é ä½ã»åºæ°ã«ã¯å ¥ããªã 2163 // ï¼prices ã¨ã¯å¥ã®é åãªã®ã§ max/min/åºæ°ã®è¨ç®ã¯è§¦ããªãï¼ãåºãæ±ã£ã¦ããååã«ã ããµã¼ãã¼ãä»ããã 2164 function kxLinkOnlyRowsHtml(product, offset) { 2165 const list = (product && product.link_only_stores) || []; 2166 if (!list.length) return ''; 2167 const _jan = (product && product.jan) || ''; 2168 const arrow = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17L17 7"/><path d="M8 7h9v9"/></svg>'; 2169 return list.map((s, j) => { 2170 const attrs = `target="_blank" ${kxStoreLinkAttrs(s.url)} onclick="if(typeof gtag==='function')gtag('event','affiliate_click',{store:'${escapeHtml(s.key)}',jan:'${escapeHtml(_jan)}',link_only:true});"`; 2171 return ` 2172 <div class="price-row stale link-only"> 2173 <div class="store-main"> 2174 <div class="store-info"> 2175 <span class="store-rank">${offset + j + 1}</span> 2176 <div class="store-name"><a href="${escapeHtml(s.url)}" ${attrs}>${escapeHtml(s.store)}</a></div> 2177 </div> 2178 <div class="store-mark-col"></div> 2179 </div> 2180 <div class="price-info"> 2181 <a class="price-site-link" href="${escapeHtml(s.url)}" ${attrs}>åºèãµã¤ãã§ç¢ºèª${arrow}</a> 2182 </div> 2183 </div>`; 2184 }).join(''); 2185 } 2186 2187 function kxStoreLinkAttrs(url, baseRel) { 2188 const rel = baseRel || 'noopener'; 2189 try { 2190 const h = new URL(url, location.href).hostname.replace(/^www\./, ''); 2191 if (KX_REFERRER_BLOCKED_HOSTS.some(b => h === b || h.endsWith('.' + b))) { 2192 return 'rel="' + rel + ' noreferrer" referrerpolicy="no-referrer"'; 2193 } 2194 } catch (e) { /* ç¸å¯¾ URL ã '#' ã¯ããã«æ¥ããæ¢å®å´ã«åã */ } 2195 return 'rel="' + rel + '" referrerpolicy="unsafe-url"'; 2196 } 2197 2198 function escapeHtml(str) { 2199 if (!str) return ''; 2200 return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 2201 } 2202 2203 function copyToClipboard(text, msg) { 2204 var toastMsg = msg || 'ã³ãã¼ãã¾ãã'; 2205 if (navigator.clipboard && navigator.clipboard.writeText) { 2206 navigator.clipboard.writeText(text).then(() => showCopiedToast(toastMsg)).catch(() => fallbackCopy(text, toastMsg)); 2207 } else { fallbackCopy(text, toastMsg); } 2208 } 2209 window.copyToClipboard = copyToClipboard; 2210 2211 function fallbackCopy(text, msg) { 2212 const textarea = document.createElement('textarea'); 2213 textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; 2214 document.body.appendChild(textarea); textarea.select(); 2215 try { document.execCommand('copy'); showCopiedToast(msg); } catch (err) { console.error('Copy failed:', err); } 2216 document.body.removeChild(textarea); 2217 } 2218 2219 function showCopiedToast(msg, durationMs) { 2220 const existing = document.querySelector('.copied-toast'); 2221 if (existing) existing.remove(); 2222 const toast = document.createElement('div'); 2223 toast.className = 'copied-toast'; 2224 toast.textContent = msg || 'ã³ãã¼ãã¾ãã'; 2225 document.body.appendChild(toast); 2226 setTimeout(() => toast.remove(), durationMs || 2000); 2227 } 2228 2229 // ==================== å©çååæ©è½ ==================== 2230 async function loadProfitableProducts() { 2231 try { 2232 const data = await apiCall('/api/profitable?page=1&per_page=50'); 2233 profitableData = data; 2234 const section = document.getElementById('profitableSection'); 2235 if (!data.products || data.products.length === 0) { 2236 section.style.display = 'none'; 2237 return; 2238 } 2239 document.getElementById('profitableBadge').textContent = `${data.total}ä»¶`; 2240 renderProfitableBanner(data.products); 2241 renderCarousel(1); 2242 // ãè²·ãå¾ã»ã¯ã·ã§ã³è¡¨ç¤º/é表示ã®è¨æ¶ã復å 2243 const saved = localStorage.getItem('kaitorix_deals_visible'); 2244 if (saved === '0') { 2245 document.getElementById('profitableContent').style.display = 'none'; 2246 document.getElementById('profitableToggle').textContent = '表示'; 2247 } 2248 } catch (err) { 2249 document.getElementById('profitableSection').style.display = 'none'; 2250 } 2251 } 2252 2253 function createCardHtml(p) { 2254 const source = p.source || 'amazon'; 2255 const isRakuten = source === 'rakuten'; 2256 function _mottokuRakuten(prod) { 2257 const su = prod.source_url || ''; 2258 const jan = prod.jan || ''; 2259 const m = su.match(/item\.rakuten\.co\.jp\/([^/]+)\//); 2260 if (jan && m && m[1]) { 2261 return window.kxRakutenUrl({ jan: jan, shop: m[1], code: (su.match(/item\.rakuten\.co\.jp\/[^/]+\/([^/?#]+)/) || [])[1] || '', query: prod.name || '' }); 2262 } 2263 return window.kxRakutenUrl({ jan: jan, query: prod.name || '' }); 2264 } 2265 const productUrl = isRakuten 2266 ? _mottokuRakuten(p) 2267 : (p.asin 2268 ? `https://www.amazon.co.jp/dp/${escapeHtml(p.asin)}?tag=${AMAZON_TAG}&linkCode=ogi&th=1&psc=1` 2269 : `https://www.amazon.co.jp/s?k=${encodeURIComponent(p.name)}`); 2270 const sourceBadge = isRakuten 2271 ? `<span style="font-size:0.7rem;font-weight:600;color:#BF0000;border:1.5px solid #BF0000;padding:2px 0;border-radius:4px;display:inline-block;width:56px;text-align:center;line-height:1.3;vertical-align:middle;">
2271楽天</span>` 2272 : `<span style="font-size:0.7rem;font-weight:600;color:#E47911;border:1.5px solid #E47911;padding:2px 0;border-radius:4px;display:inline-block;width:56px;text-align:center;line-height:1.3;vertical-align:middle;">Amazon</span>`; 2273 const imgHtml = p.image_url ? `<img src="${escapeHtml(p.image_url)}" alt="${escapeHtml(p.name || '')}" loading="lazy" width="80" height="80">` : `<svg style="width:40px;height:40px;color:var(--text-muted);opacity:0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`; 2274 return ` 2275 <div class="profit-card"> 2276 <a href="${productUrl}" target="_blank" rel="noopener" style="text-decoration:none;color:inherit;flex:1;display:flex;flex-direction:column;"> 2277 <div class="profit-card-img">${imgHtml}</div> 2278 <div class="profit-card-badge">+Â¥${p.profit.toLocaleString()}</div> 2279 <div class="profit-card-body"> 2280 <div class="profit-card-name">${escapeHtml(p.name)}</div> 2281 <div class="profit-card-price">${sourceBadge} Â¥${p.amazon_price.toLocaleString()}</div> 2282 ${p.updated ? `<div style="font-size:0.7rem;color:var(--text-muted);margin-top:2px;">${escapeHtml(p.updated)} åå¾</div>` : ''} 2283 <div class="profit-card-store">â ${escapeHtml(p.best_store)} Â¥${p.best_kaitori_price.toLocaleString()}</div> 2284 </div> 2285 </a> 2286 <a href="/dp/${escapeHtml(p.jan)}?from=profit" class="profit-card-compare" data-jan="${escapeHtml(p.jan)}">è²·åæ¯è¼ â</a> 2287 </div> 2288 `; 2289 } 2290 2291 function bindCompareButtons(container) { 2292 container.querySelectorAll('.profit-card-compare').forEach(btn => { 2293 btn.addEventListener('click', (e) => { 2294 e.preventDefault(); e.stopPropagation(); 2295 currentFrom = 'profit'; 2296 _updateBackBtn('profit'); 2297 showProduct(btn.dataset.jan); 2298 }); 2299 }); 2300 } 2301 2302 function renderProfitableBanner(products) { 2303 const container = document.getElementById('profitableBannerInner'); 2304 if (!products || products.length === 0) return; 2305 container.innerHTML = products.map(p => createCardHtml(p)).join(''); 2306 bindCompareButtons(container); 2307 } 2308 2309 function renderCarousel(page) { 2310 if (!profitableData || !profitableData.products) return; 2311 const products = profitableData.products; 2312 const totalPages = Math.ceil(products.length / CARDS_PER_PAGE); 2313 if (page < 1) page = totalPages; 2314 if (page > totalPages) page = 1; 2315 carouselPage = page; 2316 2317 const start = (page - 1) * CARDS_PER_PAGE; 2318 const pageProducts = products.slice(start, start + CARDS_PER_PAGE); 2319 const container = document.getElementById('profitableCardsRow'); 2320 container.innerHTML = pageProducts.map(p => createCardHtml(p)).join(''); 2321 bindCompareButtons(container); 2322 2323 const indicator = document.getElementById('pageIndicator'); 2324 indicator.textContent = totalPages > 1 ? `${page} / ${totalPages}` : ''; 2325 } 2326 2327 function carouselPrevPage() { renderCarousel(carouselPage - 1); } 2328 function carouselNextPage() { renderCarousel(carouselPage + 1); } 2329 2330 function toggleProfitableContent() { 2331 const content = document.getElementById('profitableContent'); 2332 const btn = document.getElementById('profitableToggle'); 2333 if (content.style.display === 'none') { 2334 content.style.display = 'block'; btn.textContent = 'é表示'; 2335 localStorage.setItem('kaitorix_deals_visible', '1'); 2336 } else { 2337 content.style.display = 'none'; btn.textContent = '表示'; 2338 localStorage.setItem('kaitorix_deals_visible', '0'); 2339 } 2340 } 2341 2342 // ==================== å©çã©ã³ãã³ã° TOP5 ãã¬ãã¥ã¼ï¼äººæ°ã¡ãã¥ã¼å·¦ï¼ ==================== 2343 async function loadProfitPreview() { 2344 try { 2345 const data = await apiCall('/api/profitable?per_page=5&preorder=false&cats=' + encodeURIComponent('ã¹ãã,ã«ã¡ã©,ã²ã¼ã ')); 2346 const card = document.getElementById('kxProfitPreview'); 2347 const list = document.getElementById('kxProfitList'); 2348 const items = (data && data.products) || []; 2349 if (!card || !list || !items.length) return; 2350 list.innerHTML = items.slice(0, 5).map((p, i) => { 2351 const img = p.image_url 2352 ? `<img src="${escapeHtml(p.image_url)}" alt="" width="40" height="40" loading="lazy">` 2353 : `<span class="kx-pv-noimg">${placeholderSVG(18)}</span>`;
2354 const store = p.best_store ? `<span class="kx-pv-store">${escapeHtml(p.best_store)}</span>` : ''; 2355 return `<a class="kx-pv-item" href="/dp/${escapeHtml(p.jan)}" data-jan="${escapeHtml(p.jan)}" title="${escapeHtml(p.name)}"> 2356 <span class="kx-pv-rank">${i + 1}</span>${img} 2357 <span class="kx-pv-body"><span class="kx-pv-name">${escapeHtml(p.name)}</span>${store}</span> 2358 <span class="kx-pv-right"> 2359 <span class="kx-pv-price">Â¥${(p.best_kaitori_price || 0).toLocaleString()}</span> 2360 <span class="kx-pv-profit">å©ç +Â¥${(p.profit || 0).toLocaleString()}</span> 2361 </span> 2362 </a>`; 2363 }).join(''); 2364 list.querySelectorAll('.kx-pv-item').forEach(a => a.addEventListener('click', (e) => { 2365 if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) return; 2366 e.preventDefault(); 2367 showProduct(a.dataset.jan, true); 2368 })); 2369 card.style.display = ''; 2370 } catch (err) { /* ãã¬ãã¥ã¼ã¯é£¾ã: 失ææã¯éãã«ã¹ããã */ } 2371 } 2372 2373 // ==================== å©çãã£ã³ã¹ tickerï¼ãã¼ã bannerç´ä¸ã»/deal å°ç·ï¼ ==================== 2374 // Amazon å©çååã® è²·åä¾¡æ ¼+å©ç ã®ã¿è¡¨ç¤ºï¼Amazon ä¾¡æ ¼ã¯åºããªãï¼ãã¯ãªãã¯ã§ /deal/{asin}ã 2375 // è²·ååºãã£ã«ã¿ã¼ï¼å©çTOPï¼/ranking/profitï¼ã¨åã localStorage kaitorix_kaitori_stores ãåç §ãã 2376 // 鏿åºã®æé«ä¾¡æ ¼ã§ è²·ååº/ä¾¡æ ¼/å©ç ãåè¨ç®ããï¼æªè¨å®=å ¨åºï¼ã 2377 function _tickerStoreFilter() { 2378 try { 2379 const v = localStorage.getItem('kaitorix_kaitori_stores'); 2380 if (!v) return null; 2381 const arr = JSON.parse(v); 2382 if (Array.isArray(arr) && arr.length) return new Set(arr); 2383 } catch (e) { /* ç ´ææã¯å ¨åºæ±ã */ } 2384 return null; 2385 } 2386 async function loadDealTicker() { 2387 const wrap = document.getElementById('kxDealTicker'); 2388 try { 2389 const track = document.getElementById('kxDealTickerTrack'); 2390 if (!wrap || !track) return; 2391 // ãã¼ã ã§ã¯ä¸èº«ãå±ãåãã帯ã®é«ããåã£ã¦ããï¼price-checker.html ã®ç´å¾ã® scriptï¼ã 2392 // å±ãåã«ååãã¾ã¨ãã¸åãæ¿ãã£ã¦ãé ããããã表示ã®è¿½å¾ã¯æåã«ç¹ãã 2393 _bindTickerVisibility(); 2394 const storeSel = _tickerStoreFilter(); 2395 const storeMap = window.KX_STORE_MAP || {}; 2396 const refine = (p) => { 2397 if (!p.asin || (p.profit || 0) <= 0 || (p.best_kaitori_price || 0) <= 0) return null; 2398 if (!storeSel) return { ...p, _store: p.best_store, _kaitori: p.best_kaitori_price, _profit: p.profit }; 2399 // 鏿åºã®ã¿ã§æé«ä¾¡æ ¼ãåãç´ããå©çã¯å·®åã§åè¨ç® 2400 let bp = 0, bk = ''; 2401 const prices = p.prices || {}; 2402 storeSel.forEach(k => { const v = prices[k] || 0; if (v > bp) { bp = v; bk = k; } }); 2403 if (bp <= 0) return null; 2404 const profit2 = (p.profit || 0) - (p.best_kaitori_price || 0) + bp; 2405 if (profit2 <= 0) return null; 2406 return { ...p, _store: storeMap[bk] || bk, _kaitori: bp, _profit: profit2 }; 2407 }; 2408 // ã«ã©ã¼éãã®åä¸ååã 1 ä»¶ã«ç³ãï¼2026-08-03 May æç¤ºï¼ãticker ã¯åååã 2409 // åºããªããããAT-LP60X ã¬ãã / ãã¼ã¯ã¬ã³ã¡ã¿ãªãã¯ãã®ããã«æ«å°¾ã®è²å 2410 // ã ããéã 2 ä»¶ã¯ãåºåã»è²·åä¾¡æ ¼ã»å©çãåä¸ã§å®å ¨ãªéè¤ã«è¦ããã 2411 // å¤å®ã¯ä¿å®çã«ãæ«å°¾ãã¼ã¯ã³ã ASCII è±æ°åãå«ã¾ãªãï¼ï¼ã«ã/æ¼¢å/[è²]表è¨ï¼
2412 // å ´åã®ã¿è½ã¨ãã¦æ¯è¼ãââåçª (MFY84J/A, CFI-7100B01) ãã2025å¹´çã㯠2413 // æ®ãã®ã§ãå¥ SKU ã誤ã£ã¦ç³ã¾ããªãã 2414 const dedupeKey = (name) => { 2415 const parts = String(name || '').trim().split(/[\sã]+/).filter(Boolean); 2416 if (parts.length > 1 && !/[0-9A-Za-z]/.test(parts[parts.length - 1])) parts.pop(); 2417 return parts.join(' '); 2418 }; 2419 // å©çä¸ä½ã¯ç¹å®ã®è²·ååºã«åãï¼2026-08-03 宿¸¬: amazon æ¯éå£ 53 ä»¶ä¸ 42 ä»¶ã 2420 // åä¸åºãã¹ãï¼ãåºãã£ã«ã¿ã¼æå¹æã«ä¸ä½20ä»¶ã ãè¦ãã¨é¸æåºã1ä»¶ãå ¥ãã 2421 // ticker ã丸ãã¨æ¶ãããããå©çTOP ã¨åããæ¯éå£ãéã£ã¦çµãè¾¼ãã 2422 const perPage = storeSel ? 50 : 20; 2423 const maxPages = storeSel ? 4 : 1; 2424 const seen = new Set(); 2425 let items = []; 2426 for (let pg = 1; pg <= maxPages; pg++) { 2427 const data = await apiCall(`/api/profitable?page=${pg}&per_page=${perPage}&source=amazon&preorder=false`); 2428 const chunk = (data && data.products) || []; 2429 for (const p of chunk) { 2430 const r = refine(p); 2431 if (!r) continue; 2432 const k = dedupeKey(r.name); 2433 if (k) { 2434 if (seen.has(k)) continue; // å ã«åºãæ¹ï¼å©çãé«ãæ¹ãæ®ã 2435 seen.add(k); 2436 } 2437 items.push(r); 2438 } 2439 if (chunk.length < perPage || items.length >= 15) break; 2440 } 2441 items = items.sort((a, b) => b._profit - a._profit).slice(0, 15); 2442 // æä½ä»¶æ°ã¯ 2ï¼æ§ 3ï¼ãã«ã©ã¼éããç³ãããã«ããçµæãåºãã£ã«ã¿ã¼ã 2443 // å¹ãã¦ããç°å¢ã§ã¯ 3 ä»¶â2 ä»¶ã«ãªã ticker ãã¨æ¶ãã¦ãã¾ãããã 2444 // ã»ããè¤è£½ã§ã«ã¼ããããã®ã§ 2 ä»¶ã§ã帯ã¯éåããªãã 2445 if (items.length < 2) { _tickerEmpty = true; wrap.style.display = 'none'; return; } 2446 _tickerEmpty = false; 2447 const itemsHtml = items.map(p => { 2448 const img = p.image_url 2449 ? `<img src="${escapeHtml(p.image_url)}" alt="" width="40" height="40" loading="lazy">` 2450 : `<span class="kx-ticker-noimg">${placeholderSVG(16)}</span>`; 2451 return `<a class="kx-ticker-item" href="/deal/${encodeURIComponent(p.asin)}?s=web" title="${escapeHtml(p.name || '')}"> 2452 ${img} 2453 <span class="kx-ticker-body"> 2454 <span class="kx-ticker-store">${escapeHtml(p._store || 'è²·åä¾¡æ ¼')}</span> 2455 <span class="kx-ticker-price">Â¥${(p._kaitori || 0).toLocaleString()}</span> 2456 </span> 2457 <span class="kx-ticker-profit">å©ç +Â¥${(p._profit || 0).toLocaleString()}</span> 2458 </a>`; 2459 }).join(''); 2460 // è¤è£½ã»ããã¯ã¹ã¯ãªã¼ã³ãªã¼ãã¼/Tab å·¡åããé¤å¤ 2461 const cloneHtml = '<div class="kx-ticker-set" aria-hidden="true">' 2462 + itemsHtml.replace(/<a class="kx-ticker-item"/g, '<a tabindex="-1" class="kx-ticker-item"') + '</div>'; 2463 track.innerHTML = '<div class="kx-ticker-set">' + itemsHtml + '</div>' + cloneHtml; 2464 wrap.style.display = ''; 2465 // 測å®ã¯è¡¨ç¤ºä¸ããåºæ¥ãªãï¼offsetWidth=0ï¼ãã颿°åããé表示ä¸ãªã表示æã«åè©¦è¡ 2466 _tickerMeasureFn = () => { 2467 const set = track.querySelector('.kx-ticker-set'); 2468 if (!set) return; 2469 const setW = set.offsetWidth; 2470 if (!setW) return; // é表示ä¸: sync() å´ããå試è¡ããã 2471 // ã·ã¼ã ã¬ã¹ã«ã¼ã: ã¢ãã¡ä¸ããã¥ã¼ãã¼ãã常ã«åã¾ãã¾ã§ã»ãããè¤è£½ 2472 const vpW = wrap.clientWidth || 0; 2473 let guard = 0; 2474 while (setW * (track.children.length - 1) < vpW && guard < 6) { 2475 track.insertAdjacentHTML('beforeend', cloneHtml); 2476 guard++; 2477 } 2478 track.style.setProperty('--kx-tk-shift', setW + 'px'); 2479 track.style.setProperty('--kx-tk-dur', Math.max(18, Math.round(setW / 32)) + 's'); 2480 }; 2481 requestAnimationFrame(_tickerMeasureFn); 2482 } catch (err) { 2483 /* ãã£ãã«ã¼ã¯é£¾ã: 失ææã¯éãã«ã¹ãããï¼åã£ã¦ãã£ãé«ãã¯ç³ãï¼ */ 2484 _tickerEmpty = true; 2485 if (wrap) wrap.style.display = 'none'; 2486 } 2487 } 2488 // ticker 㯠#searchSection ã®å¤ï¼banner ç´ä¸ï¼ã«å± ãããããã¼ã âåå/ã¾ã¨ãåæ¿ã« 2489 // èªåã§è¿½å¾ããå¿ è¦ããããsearchSection ã® inline style ãåä¸ççæºã¨ãã¦åæã 2490 // _tickerEmptyï¼åºãç©ãç¡ãï¼2 ä»¶æªæºã»åå¾å¤±æï¼ããã¼ã ã«æ»ã£ã¦ã空ã®å¸¯ãåºããªãã 2491 let _tickerVisObserver = null; 2492 let _tickerMeasureFn = null; 2493 let _tickerEmpty = false;
2494 function _bindTickerVisibility() { 2495 if (_tickerVisObserver) return; 2496 const wrap = document.getElementById('kxDealTicker'); 2497 const sec = document.getElementById('searchSection'); 2498 const track = document.getElementById('kxDealTickerTrack'); 2499 if (!wrap || !sec || !('MutationObserver' in window)) return; 2500 const sync = () => { 2501 const hidden = sec.style.display === 'none' || _tickerEmpty; 2502 wrap.style.display = hidden ? 'none' : ''; 2503 if (!hidden && track && !track.style.getPropertyValue('--kx-tk-shift') && _tickerMeasureFn) { 2504 requestAnimationFrame(_tickerMeasureFn); 2505 } 2506 }; 2507 _tickerVisObserver = new MutationObserver(sync); 2508 _tickerVisObserver.observe(sec, { attributes: true, attributeFilter: ['style'] }); 2509 sync(); 2510 } 2511 2512 // ==================== æè¿è¦ãåå + éå ±ã«ã¼ã ==================== 2513 function timeAgoJp(iso) { 2514 try { 2515 const t = new Date(iso).getTime(); 2516 const m = Math.max(1, Math.round((Date.now() - t) / 60000)); 2517 if (m < 60) return m + 'åå'; 2518 const h = Math.round(m / 60); 2519 if (h < 24) return h + 'æéå'; 2520 return Math.round(h / 24) + 'æ¥å'; 2521 } catch (e) { return ''; } 2522 } 2523 2524 async function loadSokuho(jan) { 2525 const cards = [document.getElementById('sokuhoCard'), document.getElementById('sokuhoCardMobile')].filter(Boolean); 2526 if (!cards.length) return; 2527 cards.forEach(c => { c.style.display = 'none'; }); 2528 try { 2529 const d = await apiCall('/api/product/' + jan + '/sokuho'); 2530 if (jan !== currentJan) return; 2531 // é ä¿¡å®ç¸¾ãç¡ãã¦ãã«ã¼ãã¯è¡¨ç¤ºããï¼ãã©ãã¼å°ç·ã®ããã2026-07-14 May æç¤ºï¼ 2532 const txt = (d && d.has) 2533 ? 'æçµ ' + timeAgoJp(d.last) + ' ã» ç´¯è¨ ' + d.count + ' å' 2534 : 'é ä¿¡å®ç¸¾ 0 å'; 2535 ['sokuhoMeta', 'sokuhoMetaMobile'].forEach(id => { 2536 const m = document.getElementById(id); 2537 if (m) m.textContent = txt; 2538 }); 2539 cards.forEach(c => { c.style.display = ''; }); 2540 initSokuhoFollow(jan); 2541 } catch (err) { /* éå ±æ å ±ã¯é£¾ã */ } 2542 } 2543 2544 // ==================== å¨åº«éå ±ãã©ãã¼ï¼ãã©ã¦ã¶éç¥ / Discord DMï¼ ==================== 2545 // é 信対象ããã¸å ã®ãã©ãã¼ãã¿ã³ãç¶æ æ©: 2546 // åãåãå ãªãï¼æªãã°ã¤ã³ / Discord DM ããã©ã¦ã¶éç¥ãç¡ãï¼ 2547 // â kx-push.js ã®ãåãåãæ¹ãé¸ã¶ãã¢ã¼ãã«ï¼ãã©ã¦ã¶éç¥ or Discordï¼ 2548 // åãåãå ãã â POST/DELETE /api/follow/{jan} ã§ãã°ã« 2549 // ãã©ãã¼æåç´å¾ããã®ãã©ã¦ã¶ãæªè¨å®ãªãããã®ãã©ã¦ã¶ã§ãåãåãã¾ããï¼ããåºã 2550 // åãåãå ã®å¤å®ã¯ kxPush.hasChannelï¼ãµã¼ãã¼ã® has_channel ã¨åãå½¢ï¼ã ãã使ã 2551 let _followState = null; 2552 const _FOLLOW_BELL_SVG = '<svg width="11" height="11" viewB
2552ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>'; 2553 2554 function _followHasChannel(st) { 2555 if (window.kxPush) return window.kxPush.hasChannel(st); 2556 return !!(st && st.logged_in && ((st.discord_linked && st.discord_dm !== false) || st.webpush)); 2557 } 2558 2559 function _followBtns() { 2560 return [document.getElementById('sokuhoFollowBtn'), document.getElementById('sokuhoFollowBtnMobile')].filter(Boolean); 2561 } 2562 2563 function _renderFollowBtns() { 2564 const st = _followState; 2565 _followBtns().forEach(b => { 2566 if (!st) { b.style.display = 'none'; return; } 2567 const following = !!(_followHasChannel(st) && st.following); 2568 b.classList.toggle('following', following); 2569 b.innerHTML = _FOLLOW_BELL_SVG + '<span>' + (following ? 'ãã©ãã¼ä¸' : 'ãã©ãã¼') + '</span>'; 2570 b.title = following 2571 ? 'ãã©ãã¼è§£é¤ï¼é ä¿¡æã®éç¥ã忢ï¼' 2572 : 'å¨åº«éå ±ãé ä¿¡ãããããç¥ãããã¾ãï¼ãã©ã¦ã¶éç¥ / Discordï¼'; 2573 b.style.display = ''; 2574 }); 2575 } 2576 2577 async function initSokuhoFollow(jan) { 2578 _followState = null; 2579 _followBtns().forEach(b => { b.style.display = 'none'; b.dataset.jan = jan; }); 2580 try { 2581 const res = await fetch('/api/follow/' + jan, { credentials: 'same-origin' }); 2582 if (!res.ok) return; 2583 const st = await res.json(); 2584 if (jan !== currentJan) return; 2585 _followState = st; 2586 _renderFollowBtns(); 2587 } catch (e) { /* ãã©ãã¼ãã¿ã³ã¯é£¾ã: 失ææã¯é表示ã®ã¾ã¾ */ } 2588 } 2589 2590 async function _onFollowClick(ev) { 2591 const jan = ev.currentTarget.dataset.jan || currentJan; 2592 const st = _followState; 2593 if (!st || !jan) return; 2594 if (!_followHasChannel(st)) { 2595 // åãåãå ãç¡ã â é¸ãã§ãããï¼ãã©ã¦ã¶éç¥ãªã許å¯ã¨è³¼èªã¾ã§ããã§æ¸ãï¼ 2596 if (!window.kxPush) { 2597 location.href = '/auth/discord?next=' + encodeURIComponent('/dp/' + jan) + '&follow_jan=' + jan; 2598 return; 2599 } 2600 const ch = await window.kxPush.chooseChannel({ 2601 jan: jan, loggedIn: st.logged_in, next: '/dp/' + jan, 2602 discordLinked: !!st.discord_linked, discordUsername: st.discord_username || '' 2603 }); 2604 if (!ch) return; // éãã / æªé£æºã® Discord ã¯é£æºç»é¢ã¸é·ç§»æ¸ã¿ 2605 if (ch === 'webpush') st.webpush = true; else st.discord_dm = true; 2606 st.has_channel = true; 2607 if (st.following) { 2608 _renderFollowBtns(); 2609 showCopiedToast(ch === 'webpush' ? 'ãã®ãã©ã¦ã¶ã§å¨åº«éå ±ãåãåããããã«ãªãã¾ãã' 2610 : 'Discord ã® DM ã§å¨åº«éå ±ãåãåããããã«ãªãã¾ãã', 3000); 2611 return; 2612 } 2613 } 2614 try { 2615 const res = await fetch('/api/follow/' + jan, { 2616 method: st.following ? 'DELETE' : 'POST', 2617 credentials: 'same-origin' 2618 }); 2619 if (res.status === 409) { // åãåãå ãæ¶ãã¦ããï¼å¥ç«¯æ«ã§è§£é¤ã»DM ãªãã»éç¥ã®å¤±å¹çï¼ 2620 st.has_channel = false; st.discord_dm = false; st.webpush = false; 2621 _onFollowClick({ currentTarget: ev.currentTarget }); 2622 return; 2623 } 2624 if (!res.ok) throw new Error('follow api ' + res.status); 2625 st.following = !st.following; 2626 _renderFollowBtns(); 2627 if (!st.following) { showCopiedToast('ãã©ãã¼ãè§£é¤ãã¾ãã', 3000); return; } 2628 // ãã®ãã©ã¦ã¶ãæªè¨å®ãªãããã®ãã©ã¦ã¶ã§ãåãåãã¾ããï¼ãï¼åºããæã¯ toast ãçãï¼ 2629 const offered = window.kxPush ? await window.kxPush.offerAfterFollow() : false;
2630 if (!offered) showCopiedToast('ãã©ãã¼ãã¾ãããå¨åº«éå ±ãé ä¿¡ãããããç¥ãããã¾ã', 3000); 2631 } catch (e) { 2632 showCopiedToast('éä¿¡ã¨ã©ã¼ãçºçãã¾ãããæéãããã¦ã試ããã ãã', 3000); 2633 } 2634 } 2635 2636 // Discord OAuth ã³ã¼ã«ããã¯ããã® ?discord=<flag> ã toast 表示ã㦠URL ããé¤å» 2637 function _handleDiscordFlag() { 2638 try { 2639 const sp = new URLSearchParams(location.search); 2640 const flag = sp.get('discord'); 2641 if (!flag) return; 2642 const DM_HELP = 'Discord ã®ãã©ã¤ãã·ã¼è¨å®ã§ããµã¼ãã¼ã«ããã¡ã³ãã¼ããã®ãã¤ã¬ã¯ãã¡ãã»ã¼ã¸ãã許å¯ãã¦ãã ãã'; 2643 const messages = { 2644 linked: 'Discord 飿ºãå®äºãã¾ãã', 2645 followed: 'Discord 飿ºãå®äºãããã®ååããã©ãã¼ãã¾ãããé ä¿¡æã«DMã§ãç¥ãããã¾ã', 2646 linked_dm_blocked: 'Discord 飿ºã¯å®äºãã¾ããããDMãéä¿¡ã§ãã¾ããã§ããã' + DM_HELP, 2647 followed_dm_blocked: 'ãã©ãã¼ãã¾ããããDMãéä¿¡ã§ãã¾ããã§ããã' + DM_HELP, 2648 taken: 'ãã® Discord ã¢ã«ã¦ã³ãã¯æ¢ã«å¥ã®è²·åX ã¢ã«ã¦ã³ãã«é£æºããã¦ãã¾ã', 2649 denied: 'Discord 飿ºããã£ã³ã»ã«ããã¾ãã', 2650 state_error: 'Discord 飿ºã«å¤±æãã¾ããï¼ã»ãã·ã§ã³åãï¼ãããä¸åº¦ã試ããã ãã', 2651 exchange_error: 'Discord 飿ºã«å¤±æãã¾ãããæéãããã¦ããä¸åº¦ã試ããã ãã', 2652 db_error: 'Discord 飿ºã«å¤±æãã¾ãããæéãããã¦ããä¸åº¦ã試ããã ãã', 2653 login_required: 'ãã°ã¤ã³ã»ãã·ã§ã³ãåãã¾ãããããä¸åº¦ã試ããã ãã' 2654 }; 2655 if (flag === 'linked_not_member' || flag === 'followed_not_member') { 2656 const base = flag === 'followed_not_member' 2657 ? 'ãã©ãã¼ãã¾ãããéç¥ãåãåãã«ã¯è²·åX ã® Discord ãµã¼ãã¼ã¸ã®åå ãå¿ è¦ã§ãã' 2658 : 'Discord 飿ºãå®äºãã¾ãããéç¥ãåãåãã«ã¯è²·åX ã® Discord ãµã¼ãã¼ã¸ã®åå ãå¿ è¦ã§ãã'; 2659 if (confirm(base + '\n\næå¾ ãã¼ã¸ãéãã¾ããï¼')) { 2660 window.open('https://discord.gg/VJBSYD9Jg8', '_blank', 'noopener'); 2661 } 2662 } else if (messages[flag]) { 2663 showCopiedToast(messages[flag], 5000); 2664 } 2665 sp.delete('discord'); 2666 const q = sp.toString(); 2667 history.replaceState(null, '', location.pathname + (q ? '?' + q : '') + location.hash); 2668 } catch (e) { /* 表示ã ãã®å¦ç: 失æãã¦ãå®å®³ãªã */ } 2669 } 2670 2671 document.addEventListener('DOMContentLoaded', function() { 2672 _followBtns().forEach(b => b.addEventListener('click', _onFollowClick)); 2673 _handleDiscordFlag(); 2674 }); 2675 2676 async function loadRecentViewed() { 2677 // å¤åãããã¯ã¢ãã: ãæ°ã«å ¥ã ⪠é²è¦§å±¥æ´ ã®ãã¡ã忥æ¯ã§å®éã«åãããååã ã表示ã 2678 // å ¨é¨åãã¦ãªããã°ã»ã¯ã·ã§ã³ãã¨é表示ï¼ãã¤ãºã¼ãï¼ãå±¥æ´ã¿ã°è¡=ããããã=ã¢ã©ã¼ãã¨å½¹å²åæ ã 2679 try { 2680 const sec = document.getElementById('kxRecent'); 2681 const row = document.getElementById('kxRecentRow'); 2682 if (!sec || !row) return; 2683 let items = (getFavoritesFromStorage().items || []).slice(); 2684 getHistoryFromStorage().forEach(hv => items.push({ jan: hv.jan, name: hv.name || '', image_url: hv.img || '', addedAt: hv.timestamp || 0 })); 2685 // ãã°ã¤ã³æ¸ã¿ãªããµã¼ãã®ãæ°ã«å ¥ãããã¼ã¸ 2686 try { 2687 const res = await fetch('/api/favorites', { credentials: 'same-origin' }); 2688 if (res.ok) { 2689 const sv = (await res.json()).favorites || []; 2690 sv.forEach(v => items.push({ jan: v.jan, name: v.name || '', image_url: v.image_url || '', addedAt: v.added_at ? Date.parse(v.added_at) : 0 })); 2691 } 2692 } catch (e) {} 2693 const seen = {}; 2694 items = items.sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0)) 2695 .filter(it => it.jan && !seen[it.jan] && (seen[it.jan] = 1)).slice(0, 24); // åèãå¤ãã¦ãå±¥æ´ãæ¼ãåºãããªãããæ¡å¤§ 2696 if (!items.length) { sec.style.display = 'none'; return; } 2697 let snap = {}; 2698 try { snap = await apiCall('/api/price-snap?jans=' + items.map(i => i.jan).join(',')); } catch (e) {} 2699 // å¤åãããã£ãååã ãã«çµã 2700 items = items.filter(it => (snap[it.jan] || {}).d).slice(0, 8); 2701 if (!items.length) { sec.style.display = 'none'; return; } 2702 row.innerHTML = items.map(it => { 2703 const sn = snap[it.jan] || {}; 2704 const now = sn.p || 0; 2705 const imgUrl = it.image_url || sn.img || ''; 2706 const img = imgUrl 2707 ? `<img src="${escapeHtml(imgUrl)}" alt="" width="56" height="56" loading="lazy">` 2708 : `<span class="kx-rv-noimg">${placeholderSVG(20)}</span>`;
2709 const priceHtml = now > 0 ? `<span class="kx-rv-price">Â¥${now.toLocaleString()}</span>` : ''; 2710 let diffHtml = ''; 2711 if (now > 0 && sn.d) { 2712 const pct = Math.abs(Math.round(sn.d / (now - sn.d) * 1000) / 10); 2713 diffHtml = `<span class="kx-rv-diff ${sn.d > 0 ? 'up' : 'down'}" title="忥æ¯">${sn.d > 0 ? '+' : 'â'}Â¥${Math.abs(sn.d).toLocaleString()} (${pct}%)</span>`; 2714 } 2715 return `<a class="kx-rv-item" href="/dp/${escapeHtml(it.jan)}" data-jan="${escapeHtml(it.jan)}" title="${escapeHtml(it.name)}"> 2716 ${img} 2717 <span class="kx-rv-name">${escapeHtml(it.name)}</span> 2718 ${priceHtml}${diffHtml} 2719 </a>`; 2720 }).join(''); 2721 row.querySelectorAll('.kx-rv-item').forEach(a => a.addEventListener('click', (e) => { 2722 if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) return; 2723 e.preventDefault(); 2724 showProduct(a.dataset.jan, true); 2725 })); 2726 sec.style.display = ''; 2727 } catch (err) { } 2728 } 2729 2730 // ==================== äººæ°æ¤ç´¢ã»æ¤ç´¢å±¥æ´ ==================== 2731 // åæ10件表示 â ããã£ã¨è¦ããã§ +10 ãã¤ãæå¤§100ä»¶ã¾ã§å±éããå ±éã¬ã³ãã©ã 2732 function _renderExpandableTags(container, items, from) { 2733 const CAP = Math.min(items.length, 100); 2734 let shown = Math.min(container._shownCount || 10, CAP); 2735 function paint() { 2736 container._shownCount = shown; 2737 // ã¿ã°ã® HTML 㯠price-checker.html ã® kxHintTagsHtml ãå¯ä¸ã®æ£æ¬ï¼æåã®æç»ã®åã« 2738 // åãç©ã§çµãã§ããï¼ããã§çµã¿ç´ãã¦ãè¦ãç®ã¯åããªãï¼ 2739 container.innerHTML = window.kxHintTagsHtml(items, shown); 2740 container.querySelectorAll('.hint-tag').forEach(tag => { 2741 if (tag.classList.contains('hint-more')) { 2742 tag.addEventListener('click', () => { shown = Math.min(shown + 10, CAP); paint(); }); 2743 } else if (tag.dataset.q) { 2744 // å ¨ä»¶æ¤ç´¢å±¥æ´ â /search 㸠2745 tag.addEventListener('click', () => { window.location.href = '/search?q=' + encodeURIComponent(tag.dataset.q); }); 2746 } else { 2747 tag.addEventListener('click', () => { currentFrom = from; _updateBackBtn(from); showProduct(tag.dataset.jan); }); 2748 } 2749 }); 2750 } 2751 paint(); 2752 } 2753 2754 async function loadPopularSearches() { 2755 try { 2756 // ãããã®æ®»ã«åãã¦ããã°ããã使ãï¼åã /api/popular ã®ä¸èº«ã»1 åã ãï¼ 2757 const embedded = window.KX_POPULAR; 2758 window.KX_POPULAR = null; 2759 const data = (embedded && embedded.popular && embedded.popular.length) 2760 ? embedded : await apiCall('/api/popular'); 2761 if (data.popular && data.popular.length > 0) { 2762 _renderExpandableTags(document.getElementById('popularTags'), data.popular, 'popular'); 2763 document.getElementById('popularSearches').style.display = 'flex'; 2764 } 2765 } catch (err) { } 2766 } 2767 2768 function loadHistory() { 2769 const history = getHistoryFromStorage(); 2770 if (history.length > 0) { 2771 _renderExpandableTags(document.getElementById('historyTags'), history, 'history'); 2772 document.getElementById('historySection').style.display = 'flex'; 2773 } else { 2774 document.getElementById('historySection').style.display = 'none'; 2775 } 2776 } 2777 2778 // ==================== ã«ãã´ãªã¿ãã»æãããã¿ ==================== 2779 let _categoryCache = null; 2780 let _currentPopularTab = 'search_count'; 2781 let _currentCategoryId = null; 2782 let _currentCategoryName = null; 2783 2784 async function loadCategoryChips() { 2785 if (_categoryCache) return _categoryCache; 2786 try { 2787 const data = await apiCall('/api/trend_categories'); 2788 _categoryCache = (data && data.categories) ? data.categories : []; 2789 return _categoryCache; 2790 } catch (err) { return []; } 2791 } 2792 2793 async function renderCategoryChips() { 2794 const container = document.getElementById('categoryChipsContainer'); 2795 if (!container) return; 2796 if (container.dataset.rendered === '1') return; 2797 const cats = await loadCategoryChips(); 2798 if (!cats.length) { 2799 container.innerHTML = '<span style="color:var(--text-muted);font-size:0.85rem;padding:6px 0;">ã«ãã´ãªãªã</span>'; 2800 container.dataset.rendered = '1'; 2801 return; 2802 } 2803 container.innerHTML = cats.map(c => 2804 `<span class="hint-tag" data-category-id="${c.id}" data-category-name="${escapeHtml(c.keyword_ja)}">${escapeHtml(c.keyword_ja)}</span>` 2805 ).join(''); 2806 container.querySelectorAll('.hint-tag').forEach(chip => { 2807 chip.addEventListener('click', () => { 2808 selectCategory(chip.dataset.categoryId, chip.dataset.categoryName); 2809 }); 2810 }); 2811 container.dataset.rendered = '1'; 2812 } 2813 2814 let _categoryResults = []; 2815 let _categoryHasMore = true; 2816 let _categoryLoading = false;
2817 const _CATEGORY_PAGE = 30; 2818 2819 async function selectCategory(categoryId, categoryName) { 2820 _currentCategoryId = categoryId; 2821 _currentCategoryName = categoryName; 2822 _categoryResults = []; 2823 _categoryHasMore = true; 2824 _categoryLoading = false; 2825 await fetchCategoryPage(); 2826 } 2827 2828 async function fetchCategoryPage() { 2829 if (_categoryLoading || !_categoryHasMore || !_currentCategoryId) return; 2830 _categoryLoading = true; 2831 try { 2832 const offset = _categoryResults.length; 2833 const data = await apiCall(`/api/products/by_category?category_id=${encodeURIComponent(_currentCategoryId)}&limit=${_CATEGORY_PAGE}&offset=${offset}`); 2834 const more = (data && data.products) ? data.products : []; 2835 _categoryResults = _categoryResults.concat(more); 2836 _categoryHasMore = !!(data && data.has_more); 2837 } catch (err) { _categoryHasMore = false; } 2838 _categoryLoading = false; 2839 renderCategoryDropdown(); 2840 } 2841 2842 function renderCategoryDropdown() { 2843 if (!elements.searchDropdown) return; 2844 if (!_categoryResults.length) { 2845 elements.searchDropdown.innerHTML = '<div class="dropdown-empty">該å½ååãªã</div>'; 2846 elements.searchDropdown.classList.add('active'); 2847 return; 2848 } 2849 let html = _categoryResults.map(p => { 2850 const img = p.image_url 2851 ? `<img src="${escapeHtml(p.image_url)}" loading="lazy" alt="" style="width:36px;height:36px;object-fit:contain;border-radius:4px;background:#fff;flex-shrink:0;">` 2852 : `<div style="width:36px;height:36px;border-radius:4px;background:var(--border);flex-shrink:0;"></div>`; 2853 return `<div class="dropdown-item" data-jan="${escapeHtml(p.jan)}" style="display:flex;align-items:center;gap:10px;"> 2854 ${img} 2855 <div style="flex:1;min-width:0;"> 2856 <div class="dropdown-item-name">${escapeHtml((p.name || '').substring(0,80))}</div> 2857 <div class="dropdown-item-code">JAN: ${escapeHtml(p.jan)}</div> 2858 </div> 2859 </div>`; 2860 }).join(''); 2861 if (_categoryHasMore) { 2862 const label = _categoryLoading ? 'èªã¿è¾¼ã¿ä¸...' : `ããã«èªã¿è¾¼ã (+${_CATEGORY_PAGE})`; 2863 html += `<div id="categoryLoadMore" style="padding:10px;text-align:center;cursor:pointer;color:var(--primary);font-weight:600;font-size:0.85rem;border-top:1px solid var(--border);">${label}</div>`; 2864 } 2865 elements.searchDropdown.innerHTML = html; 2866 elements.searchDropdown.classList.add('active'); 2867 elements.searchDropdown.querySelectorAll('.dropdown-item').forEach(el => { 2868 el.addEventListener('click', () => { 2869 elements.searchDropdown.classList.remove('active'); 2870 currentFrom = 'popular'; _updateBackBtn('popular'); 2871 showProduct(el.dataset.jan); 2872 }); 2873 }); 2874 const moreBtn = document.getElementById('categoryLoadMore'); 2875 if (moreBtn) { 2876 moreBtn.addEventListener('click', (e) => { 2877 e.stopPropagation(); 2878 fetchCategoryPage(); 2879 }); 2880 } 2881 } 2882 2883 async function switchPopularTab(tabName) { 2884 _currentPopularTab = tabName; 2885 try { localStorage.setItem('kaitorix_popular_tab', tabName); } catch(e) {} 2886 const tSearch = document.getElementById('tabSearchCount'); 2887 const tCat = document.getElementById('tabCategory'); 2888 const popTags = document.getElementById('popularTags'); 2889 const catCont = document.getElementById('categoryChipsContainer'); 2890 if (!tSearch || !tCat || !popTags || !catCont) return; 2891 if (tabName === 'search_count') { 2892 tSearch.classList.add('is-active'); 2893 tCat.classList.remove('is-active'); 2894 popTags.style.display = 'flex'; 2895 catCont.style.display = 'none'; 2896 } else { 2897 tCat.classList.add('is-active'); 2898 tSearch.classList.remove('is-active'); 2899 popTags.style.display = 'none'; 2900 catCont.style.display = 'flex'; 2901 await renderCategoryChips(); 2902 } 2903 } 2904 2905 function toggleCollapse(bodyId, btnId, storageKey) { 2906 const body = document.getElementById(bodyId); 2907 const btn = document.getElementById(btnId); 2908 if (!body || !btn) return; 2909 const isCollapsed = body.classList.toggle('is-hidden'); 2910 btn.classList.toggle('is-collapsed', isCollapsed); 2911 btn.setAttribute('aria-expanded', String(!isCollapsed)); 2912 try { localStorage.setItem(storageKey, isCollapsed ? '1' : '0'); } catch(e) {} 2913 } 2914 2915 function restoreCollapsedState() { 2916 try { 2917 if (localStorage.getItem('kaitorix_popular_collapsed') === '1') { 2918 const body = document.getElementById('popularBody'); 2919 const btn = document.getElementById('popularCollapseBtn'); 2920 if (body) body.classList.add('is-hidden'); 2921 if (btn) { btn.classList.add('is-collapsed'); btn.setAttribute('aria-expanded', 'false'); } 2922 } 2923 if (localStorage.getItem('kaitorix_history_collapsed') === '1') { 2924 const body = document.getElementById('historyBody'); 2925 const btn = document.getElementById('historyCollapseBtn'); 2926 if (body) body.classList.add('is-hidden'); 2927 if (btn) { btn.classList.add('is-collapsed'); btn.setAttribute('aria-expanded', 'false'); } 2928 } 2929 const savedTab = localStorage.getItem('kaitorix_popular_tab'); 2930 if (savedTab === 'category') { 2931 switchPopularTab('category'); 2932 } 2933 } catch (e) {} 2934 } 2935 2936 function getHistoryFromStorage() { 2937 try { 2938 const data = localStorage.getItem(HISTORY_KEY); 2939 if (!data) return []; 2940 const history = JSON.parse(data); 2941 const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; 2942 return history.filter(item => item.timestamp > weekAgo); 2943 } catch (err) { return []; } 2944 } 2945 2946 function saveToHistory(jan, name, extra) { 2947 try { 2948 let history = getHistoryFromStorage().filter(item => item.jan !== jan); 2949 const entry = { jan, name, timestamp: Date.now() }; 2950 if (extra) { 2951 if (extra.price > 0) entry.price = extra.price; // é²è¦§æç¹ã®æé«è²·åä¾¡æ ¼ 2952 if (extra.img) entry.img = extra.img; 2953 if (extra.cat) entry.cat = extra.cat; // ã«ãã´ãªå好ã®èç© 2954 } 2955 history.unshift(entry); 2956 history = history.slice(0, MAX_HISTORY_ITEMS); 2957 localStorage.setItem(HISTORY_KEY, JSON.stringify(history)
2957); 2958 loadHistory(); 2959 // ãã°ã¤ã³æ¸ã¿ãªããµã¼ãã«ãä¿åï¼ç«¯æ«ã¾ããï¼ãå¿åã¯é»ã£ã¦ã¹ãããã 2960 // ãã°ã¤ã³å¤å®ã¯ /api/auth/me ã§ç¢ºå®ããå°ï¼data-authï¼ã§è¦ããã¢ãã¿ã¼æ ã¯å¸¸æè¡¨ç¤ºã§ã 2961 // is-guest ã localStorage ã®å å½ã¦ã§å¤ããï¼ã©ã¡ããéåºå¾ã»æéåãã§åãã¤ãï¼2026-09-26 reviewï¼ 2962 const avatarBtn = document.getElementById('userAvatarBtn'); 2963 if (avatarBtn && avatarBtn.dataset.auth === 'user') { 2964 fetch('/api/me/recent-views', { 2965 method: 'POST', credentials: 'same-origin', 2966 headers: { 'Content-Type': 'application/json' }, 2967 body: JSON.stringify({ jan, name, ts: entry.timestamp, 2968 price: entry.price || 0, img: entry.img || '', cat: entry.cat || '' }) 2969 }).catch(() => {}); 2970 } 2971 } catch (err) { } 2972 } 2973 2974 function clearHistory() { 2975 localStorage.removeItem(HISTORY_KEY); 2976 document.getElementById('historySection').style.display = 'none'; 2977 } 2978 2979 // ==================== ã¾ã¨ãæ©è½ (Matome Cart) ==================== 2980 function getMatomeFromStorage() { 2981 try { 2982 const data = localStorage.getItem(MATOME_KEY); 2983 return data ? JSON.parse(data) : { items: [], updatedAt: null }; 2984 } catch (err) { return { items: [], updatedAt: null }; } 2985 } 2986 2987 function saveMatomeToStorage(matome) { 2988 matome.updatedAt = Date.now(); 2989 localStorage.setItem(MATOME_KEY, JSON.stringify(matome)); 2990 } 2991 2992 function addToMatome(jan, name, quantity, image_url) { 2993 const matome = getMatomeFromStorage(); 2994 const existingIndex = matome.items.findIndex(item => item.jan === jan); 2995 if (existingIndex >= 0) { 2996 matome.items[existingIndex].quantity += quantity; 2997 if (matome.items[existingIndex].quantity > 999) matome.items[existingIndex].quantity = 999; 2998 saveMatomeToStorage(matome); 2999 return { success: true, message: `æ°éãæ´æ°ãã¾ããï¼åè¨ ${matome.items[existingIndex].quantity}åï¼` }; 3000 } 3001 if (matome.items.length >= MAX_MATOME_ITEMS) return { success: false, message: `æå¤§${MAX_MATOME_ITEMS}種é¡ã¾ã§ã§ã` }; 3002 matome.items.push({ jan, name, quantity, image_url, addedAt: Date.now() }); 3003 saveMatomeToStorage(matome); 3004 return { success: true, message: `${quantity}å追å ãã¾ãã` }; 3005 } 3006 3007 function updateMatomeItemQuantity(jan, quantity) { 3008 const matome = getMatomeFromStorage(); 3009 const item = matome.items.find(item => item.jan === jan); 3010 if (item) { item.quantity = Math.max(1, Math.min(999, quantity)); saveMatomeToStorage(matome); } 3011 updateMatomeUI(); 3012 } 3013 3014 function removeFromMatome(jan) { 3015 const matome = getMatomeFromStorage(); 3016 matome.items = matome.items.filter(item => item.jan !== jan); 3017 saveMatomeToStorage(matome); 3018 updateMatomeUI(); 3019 } 3020 3021 function clearMatome() { 3022 if (!confirm('ã¾ã¨ããªã¹ãããã¹ã¦åé¤ãã¾ããï¼')) return; 3023 saveMatomeToStorage({ items: [], updatedAt: Date.now() }); 3024 collapseMatomeBar(); 3025 updateMatomeUI(); 3026 } 3027 3028 function updateMatomeUI() { 3029 const matome = getMatomeFromStorage(); 3030 const count = matome.items.length; 3031 const totalQty = matome.items.reduce((sum, item) => sum + item.quantity, 0); 3032 3033 const bar = document.getElementById('matomeBar'); 3034 const fab = document.getElementById('matomeCartFab'); 3035 const popover = document.getElementById('matomeCartPopover'); 3036 3037 if (count === 0) { 3038 bar.style.display = 'none'; 3039 document.body.classList.remove('has-matome-bar'); 3040 if (fab) { fab.classList.remove('visible'); fab.style.display = 'none'; } 3041 if (popover) popover.classList.remove('open'); 3042 } else { 3043 bar.style.display = 'block'; 3044 document.body.classList.add('has-matome-bar'); 3045 document.getElementById('matomeItemCount').textContent = count; 3046 document.getElementById('matomeTotalQuantity').textContent = totalQty; 3047 if (fab) { 3048 fab.classList.add('visible'); 3049 fab.style.display = 'flex'; 3050 const badge = document.getElementById('matomeCartBadge'); 3051 if (badge) { badge.textContent = count; badge.style.display = 'flex'; } 3052 } 3053 const popoverCount = document.getElementById('matomePopoverCount'); 3054 const popoverTotalQty = document.getElementById('matomePopoverTotalQty'); 3055 if (popoverCount) popoverCount.textContent = count; 3056 if (popoverTotalQty) popoverTotalQty.textContent = totalQty; 3057 renderMatomeItemsList(); 3058 } 3059 } 3060 3061 function renderMatomeItemsList() { 3062 const matome = getMatomeFromStorage(); 3063 const itemsHtml = matome.items.map(item => { 3064 const imgHtml = item.image_url ? `<img src="${escapeHtml(item.image_url)}" alt="${escapeHtml(item.name || '')}" width="40" height="40">` : `<svg class="ui-icon" style="opacity:0.3" viewBox="0 0 24 24">
3064<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`; 3065 return ` 3066 <div class="matome-item" data-jan="${escapeHtml(item.jan)}"> 3067 <div class="matome-item-img">${imgHtml}</div> 3068 <div class="matome-item-info"> 3069 <div class="matome-item-name">${escapeHtml(item.name)}</div> 3070 <div class="matome-item-jan">JAN: ${escapeHtml(item.jan)}</div> 3071 </div> 3072 <div class="matome-item-controls"> 3073 <div class="matome-item-qty"> 3074 <button class="matome-qty-minus">-</button> 3075 <input type="number" class="matome-qty-input" value="${item.quantity}" min="1" max="999"> 3076 <button class="matome-qty-plus">+</button> 3077 </div> 3078 <button class="matome-item-delete">â</button> 3079 </div> 3080 </div> 3081 `; 3082 }).join(''); 3083 document.getElementById('matomeItemsList').innerHTML = itemsHtml; 3084 const popoverItems = document.getElementById('matomePopoverItems'); 3085 if (popoverItems) popoverItems.innerHTML = itemsHtml; 3086 bindMatomeItemEvents(); 3087 } 3088 3089 function bindMatomeItemEvents() { 3090 document.querySelectorAll('.matome-item').forEach(el => { 3091 const jan = el.dataset.jan; 3092 const input = el.querySelector('.matome-qty-input'); 3093 el.querySelector('.matome-qty-minus').addEventListener('click', () => { input.value = Math.max(1, parseInt(input.value) - 1); updateMatomeItemQuantity(jan, input.value); }); 3094 el.querySelector('.matome-qty-plus').addEventListener('click', () => { input.value = Math.min(999, parseInt(input.value) + 1); updateMatomeItemQuantity(jan, input.value); }); 3095 input.addEventListener('change', () => { input.value = Math.max(1, Math.min(999, parseInt(input.value) || 1)); updateMatomeItemQuantity(jan, input.value); }); 3096 el.querySelector('.matome-item-delete').addEventListener('click', () => removeFromMatome(jan)); 3097 }); 3098 } 3099 3100 function expandMatomeBar() { 3101 matomeBarExpanded = true; 3102 document.getElementById('matomeBarCollapsed').style.display = 'none'; 3103 document.getElementById('matomeBarExpanded').style.display = 'flex'; 3104 renderMatomeItemsList(); 3105 } 3106 3107 function collapseMatomeBar() { 3108 matomeBarExpanded = false; 3109 document.getElementById('matomeBarCollapsed').style.display = 'flex'; 3110 document.getElementById('matomeBarExpanded').style.display = 'none'; 3111 } 3112 3113 async function startMatomeComparison() { 3114 const matome = getMatomeFromStorage(); 3115 if (matome.items.length === 0) return; 3116 3117 // GA4: begin_checkout (ã¾ã¨ãæ¯è¼éå§) 3118 if (window.gtag) gtag('event', 'begin_checkout', { 3119 items: matome.items.map(i => ({item_id: i.jan, quantity: i.quantity})) 3120 }); 3121 3122 previousViewState = elements.productSection.classList.contains('active') ? { type: 'product', jan: currentJan } : { type: 'search' }; 3123 elements.searchSection.style.display = 'none'; 3124 elements.productSection.classList.remove('active'); 3125 // ã¾ã¨ãæ¯è¼ç»é¢ã§ã¯ãå¤åããã人æ°ã¡ãã¥ã¼ãã»ã¯ã·ã§ã³ãé ãï¼æ»ãæã«å¾©å¸°ï¼ 3126 const kxQuickSection = document.querySelector('.kx-quick-section'); 3127 if (kxQuickSection) kxQuickSection.style.display = 'none'; 3128 // æ¯è¼çµæã®ä¸ã«ãµã¤ãç´¹ä»ã丸ãã¨ç¶ãã¨ãçµæãèªã¿çµããå°ç·ãåããã 3129 // ã³ãã¥ããã£ã¸ã®èªå°ï¼tg-dc-gridï¼ã ãæ®ãã¦ä»ã¯ç³ã 3130 toggleAboutForMatome(true); 3131 // ç¢å°ã¨ããã㯠#heroCarousel ã®å¤ï¼.hero-wrap ç´ä¸ã® #heroControlsï¼ã«ããã®ã§ 3132 // ã«ã«ã¼ã»ã«ã ãé ãã¨æä½ç³»ãæ®ããã©ããã¼ãã¨ç³ã 3133 const heroWrap = document.querySelector('.hero-wrap'); 3134 if (heroWrap) heroWrap.style.display = 'none'; 3135 // æ¯è¼çµæã®ä¸ã«æµ®ããã®ã¯åºããªãï¼May æç¤º 2026-08-14ï¼ã 3136 // ãã ã **ã¾ã¨ã FAB ã ãã¯æ®ã**ï¼May æç¤º 2026-08-19ï¼ââæ¯è¼ããã¾ã¾ 3137 // ä¸èº«ã確èªã»å¢æ¸ã§ããå°ç·ãç¡ãã¨ããªã¹ããç´ãã®ã«ä¸åº¦æ»ããããªãã 3138 document.querySelectorAll('.discord-fab, .x-fab').forEach(el => { el.style.display = 'none'; });
3139 // 対象ååã¯å ¥ããã³ã«ç³ãã ç¶æ ãããæ¢å®å¤ã ãå¤ãã¦ããååéãã¦ãããã 3140 // ä¿åããã¦ãã人ã«ã¯å¹ããã225 種é¡ã ã¨çµæã¾ã§è¾¿ãçããªãï¼ä¿åã¯ããªãï¼ 3141 splitSettings.itemsCollapsed = true; 3142 document.getElementById('matomeBar').style.display = 'none'; 3143 document.body.classList.remove('has-matome-bar'); 3144 const fab = document.getElementById('matomeCartFab'); 3145 const popover = document.getElementById('matomeCartPopover'); 3146 // ãã®ç»é¢ã§ã¯ DC/X FAB ãåºããªãï¼å³ä¸ã®å¸ã空ãã®ã§ãã¾ã¨ã FAB ã 3147 // æ¬æ¥ã®ãå¸ +68pxãããè§ã®å¸ã¸å¯ããï¼CSS: body.kx-matome-compareï¼ã 3148 // ããããªã¼ãã¼ã¯å ããè§ã«ç½®ããã¦ããã®ã§ãå¯ããæ¹ãç¸¦ã«æãã 3149 document.body.classList.add('kx-matome-compare'); 3150 if (fab && matome.items.length > 0) { 3151 fab.classList.add('visible'); 3152 fab.style.display = 'flex'; 3153 const cartBadge = document.getElementById('matomeCartBadge'); 3154 if (cartBadge) { cartBadge.textContent = matome.items.length; cartBadge.style.display = 'flex'; } 3155 } 3156 if (popover) popover.classList.remove('open'); 3157 collapseMatomeBar(); 3158 showLoading(true); 3159 3160 try { 3161 fetch('/api/matome/log', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({count: matome.items.length})}).catch(() => {}); 3162 const products = await fetchProductsBatch(matome.items.map(item => item.jan)); 3163 const validProducts = products.filter(p => p !== null); 3164 if (validProducts.length === 0) throw new Error('ååãã¼ã¿ãåå¾ã§ãã¾ããã§ãã'); 3165 3166 const storeResults = calculateStoreResults(matome.items, validProducts); 3167 renderMatomeComparison(matome.items, validProducts, storeResults); 3168 3169 showLoading(false); 3170 document.getElementById('matomeResultSection').style.display = 'block'; 3171 // æ¯è¼çµæç»é¢ã§ã¯ä¸ã®ãã¾ã¨ã â¯ä»¶ï¼ä¸æ¬æ¯è¼ããããã¼ã¯åºããªãã 3172 // ä»ã¾ãã«è¦ã¦ãããã®ãããä¸åº¦å®è¡ãããã¿ã³ã§ãä¸ç«¯ãå¡ãã ãã ã£ãã 3173 // has-matome-bar ãæ®ãã¨ãã¼ãç¡ãã®ã«ä¸ä½ç½ã ã空ãã®ã§ä¸ç·ã«å¤ã 3174 document.getElementById('matomeBar').style.display = 'none'; 3175 document.body.classList.remove('has-matome-bar'); 3176 // URLã«matome=compareã追å ï¼ãªãã¼ãæã«èªåå¾©å¸°ï¼ 3177 if (!new URLSearchParams(location.search).has('matome')) { 3178 const u = new URL(location.href); u.searchParams.set('matome', 'compare'); 3179 history.replaceState(null, '', u.toString()); 3180 } 3181 3182 // ã¾ã¨ãURLå ±æãã¿ã³ããã¼ã®ä¸ã®éçãã¿ã³ãåºãã ãã«ãã 3183 const shareUrl = encodeMatomToUrl(); 3184 const shareBtn = document.getElementById('matomeShareBtn'); 3185 if (shareBtn) { 3186 shareBtn.hidden = !shareUrl; 3187 shareBtn.onclick = shareUrl ? function () { 3188 copyToClipboard(shareUrl, 'ã¾ã¨ãæ¯è¼ã®URLãã³ãã¼ãã¾ãã'); 3189 shareBtn.classList.add('is-done'); 3190 setTimeout(() => shareBtn.classList.remove('is-done'), 2000); 3191 } : null; 3192 } 3193 } catch (err) { 3194 showLoading(false); showError(err.message); goBackFromMatomeComparison(); 3195 } 3196 } 3197 3198 const MATOME_STORE_NAMES = KX_STORE_NAME_BY_KEY; 3199 let bestFilterStores = (() => { 3200 try { const s = JSON.parse(localStorage.getItem('kaitorix_bestFilter')); if (Array.isArray(s)) return new Set(s); } catch {} 3201 return new Set(Object.values(MATOME_STORE_NAMES)); 3202 })(); 3203 function saveBestFilter() { try { localStorage.setItem('kaitorix_bestFilter', JSON.stringify([...bestFilterStores])); } catch {} } 3204 3205 // ===== ã¾ã¨ãè²·åã®ãåãã¦å£²ããæé©å ===== 3206 // 1åºã«ã¾ã¨ããããè¤æ°åºã«åããæ¹ãæåããå¢ãããã¨ããã䏿¹ãç®±ãå¢ããã¶ã 3207 // éæã¨æ¯è¾¼ææ°æãããããå ã®åºã®éæç¡ææ¡ä»¶ãå²ã£ã¦ãã¾ããã¨ãããã 3208 // å ¨çµåãï¼æå¤§3åºï¼4,991éãï¼ãç´ ç´ã«ç·å½ãããã¦æåããæå¤§ã«ãªãåãæ¹ãåºãã
3209 // 宿¸¬ 20ç¹ã§ã 25ms ç¨åº¦ãªã®ã§ãåªå®ã¯ããªãï¼æé«å¤ãæã¤åºã ãã«çµãã¨æé©è§£ãåãéãï¼ã 3210 // åºèããã㯠SP ã 㨠31 åã§ 1 ç»é¢ãåããã®ã§ãçãç»é¢ã¯åæç¶æ ã§ç³ãã§ãã 3211 const SPLIT_DEFAULTS = { maxStores: 2, shipping: true, shippingCost: 1100, transferFee: true, 3212 walkin: false, 3213 itemsCollapsed: true, storesCollapsed: window.matchMedia('(max-width: 768px)').matches }; 3214 3215 // ===== åºé ã«æã¡è¾¼ãï¼åºé è²·åï¼ã¢ã¼ã ===== 3216 // èªåã§åºã¾ã§éã¶ã®ã§éæã¯ä¸åæãããããã®å ´ã§ç¾éãåãåãã®ãåºæ¬ãªã®ã§ 3217 // æ¯è¾¼ææ°æãæãããªããããã«ä¸é¨ã®åºã¯ãæ¥åº+200åãã®ããã«**æã¡è¾¼ãã æã ã** 3218 // å価ãä¸ããï¼DB ã®è²·å価ã¯éµéåºæºã®ã¾ã¾ï¼May 2026-07-23 è£æ±ºãå ç®ã¯ 3219 // product_mark_notes ã®åèããåãåºã㦠walkin_bonus ã§éã£ã¦ããï¼ã 3220 // éæã»æ¯è¾¼ææ°æã®ãã°ã«ã¯å¤ãä¿ã£ãã¾ã¾ããã® 2 æ¬ã®åºå£ã ãã§ç¡å¹åããã 3221 // ããããªãã¨ã¢ã¼ããæ»ããæã«å©ç¨è ã®è¨å®ãæ¶ããã 3222 // åºé è²·åã®æ ç¹ãããåºã ããæã¡è¾¼ã¿å ã«ãªãï¼å® é å°éã®åºãéæ0ã§ä¸¦ã¹ã㨠3223 // åå¨ããªãå¾ãè¦ãããã¨ã«ãªãï¼ãçæºã¯ shop_snapshots ã® layer7_locationsã 3224 function splitWalkinMap() { return (window.KX_SHIPPING_RULES || {}).walkin || {}; } 3225 function splitCanWalkin(storeKey) { return !!splitWalkinMap()[storeKey]; }
3226 // ð´ ãåºé ã¢ã¼ãããã®å¤å®ã¯ãã® 1 æ¬ã ãã`splitSettings.walkin` ãç´ã«è¦ã¦ã¯ 3227 // ãããªãââæ ç¹ãã¼ã¿ã空ï¼snapshot ãèªããªãçã_load_walkin() ã¯æ¡ãæ½°ã㦠3228 // {} ãè¿ãï¼ã ã¨ç»é¢å´ã¯å ¨é¨ãåºé ã¢ã¼ãã§ã¯ãªããã¨ãã¦æãã®ã«ãè¨ç®å´ã ãã 3229 // ON ã®ã¾ã¾ã«ãªãããããªãã¨éæãé»ã£ã¦è¨ç®ããå¤ããéæãã°ã«ã¯ ON ã§ 3230 // æå¹ã«è¦ããã®ã«æ¼ãã¦ãä½ãå¤ããããããã åºé ã«æã¡è¾¼ã ãã¿ã³èªä½ã 3231 // æãããªãã®ã§**ãã©ã°ãæ»ãææ®µãç¡ã**ï¼è¨å®ã¯ä¿åãããã®ã§ãªãã¼ãã§ã 3232 // ç´ããªãï¼ãsplitRenderEmpty ã§å¡ãã ã®ã¨åããè©°ã¿ãããã䏿¹ã®æããå ¥ãã 3233 function splitWalkinOn() { 3234 return !!splitSettings.walkin && Object.keys(splitWalkinMap()).length > 0; 3235 } 3236 function splitEffShipCost() { 3237 if (splitWalkinOn()) return null; 3238 return splitSettings.shipping ? splitSettings.shippingCost : null; 3239 } 3240 function splitEffFeeOn() { return !splitWalkinOn() && !!splitSettings.transferFee; } 3241 // ãæ¥åºå¢é¡ã¯ãããéé¡ãæ¸ããã¦ããªããåèï¼æ£®æ£®ã® 3242 // ãâ»æ¥åºå¢é¡éé¡ã¯å¨åº«ç¶æ³ã«ããå¤åãããã¾ãããªã©ï¼ãæ°åãä½ããã«æ³¨è¨ã ãåºãã 3243 // ð´ ãã ã® /æ¥åº/ ã§è¦ã¦ã¯ãããªãââã*忏ç°éµé&æ¬é¨æ¥åºéå®*ãã¯å ç®ã§ã¯ãªã 3244 // å仿¡ä»¶ã§ãæ¾ãã¨å ¨ãå¥ã®æå³ã®æ³¨è¨ãåºãã 3245 const KX_WALKIN_VAGUE_RE = /æ¥åº\s*(?:å¢é¡|å ç®|ã¢ãã|UP)/i; 3246 // æ ç¹ã®è¦ç´ï¼ãæ±äº¬2ã»å¤§éª1ãï¼ãareas 㯠snapshot ã®ãæ±äº¬ 2ãå½¢å¼ãªã®ã§ç©ºç½ãè©°ãã 3247 function splitWalkinAreas(storeKey) { 3248 const w = splitWalkinMap()[storeKey]; 3249 if (!w) return ''; 3250 const a = (w.areas || []).map(x => String(x).replace(/\s+/g, '')); 3251 // 空ãªãä½ãè¶³ããªããå¼ã³åºãå´ãæ¢ã«ãNåºèãã¨åºãã¦ããã®ã§ã 3252 // ããã§æ ç¹æ°ãç¹°ãè¿ãã¨ã3åºèï¼3æ ç¹ï¼ãã«ãªã 3253 if (!a.length) return ''; 3254 // ð´ ãã»ã7ãã¨åä½ãçãã¨ãæ¬å¼§ã®ä¸ã®æ°åãåºèæ°ãªã®ã§ 3255 // ããã¨7åºèãã¨èªããï¼å®é㯠7 ã¨ãªã¢ï¼æ±äº¬4ã»åè3â¦ã®ç¶ãï¼ã 3256 // /shops ã®æ ç¹ã«ã¼ãã¨åãããã»ãâ¯ã¨ãªã¢ãã¨åä½ãæ¸ã 3257 return a.length > 4 ? a.slice(0, 4).join('ã»') + `ã»ã${a.length - 4}ã¨ãªã¢` : a.join('ã»'); 3258 } 3259 3260 let splitSettings = (() => { 3261 try { 3262 const s = JSON.parse(localStorage.getItem('kaitorix_splitSettings')); 3263 if (s && typeof s === 'object') return Object.assign({}, SPLIT_DEFAULTS, s); 3264 } catch {} 3265 return Object.assign({}, SPLIT_DEFAULTS); 3266 })(); 3267 3268 function saveSplitSettings() { 3269 try { localStorage.setItem('kaitorix_splitSettings', JSON.stringify(splitSettings)); } catch {} 3270 // ãã°ã¤ã³ä¸ãªãã¢ã«ã¦ã³ãå´ã«ãä¿åãã¦ç«¯æ«ãã¾ããã§åãè¨å®ã使ããããã«ãã 3271 fetch('/api/user-settings', { 3272 method: 'PUT', credentials: 'same-origin', 3273 headers: { 'Content-Type': 'application/json' }, 3274 body: JSON.stringify({ split_settings: splitSettings }) 3275 }).catch(() => {}); 3276 } 3277 3278 function loadSplitSettingsFromAccount() { 3279 return fetch('/api/user-settings', { credentials: 'same-origin' }) 3280 .then(r => (r.ok ? r.json() : null)) 3281 .then(s => { 3282 if (s && s.split_settings && typeof s.split_settings === 'object') { 3283 splitSettings = Object.assign({}, SPLIT_DEFAULTS, s.split_settings); 3284 try { localStorage.setItem('kaitorix_splitSettings', JSON.stringify(splitSettings)); } catch {} 3285 } 3286 }) 3287 .catch(() => {}); 3288 } 3289 3290 // --- ã«ã¼ã«å¤å® --- 3291 function splitItemMatches(item, cond) { 3292 if (cond.categories && cond.categories.indexOf(item.category) === -1) return false;
3293 if (cond.include_name_patterns && 3294 !cond.include_name_patterns.some(p => (item.name || '').toLowerCase().includes(p.toLowerCase()))) return false; 3295 return true; 3296 } 3297 3298 // ãã®ååãç¹æ°ã«æ°ãããï¼è²·åwiki ã®ç¸ç´é¡ããã©ã´ã³ã®ã²ã¼ã æ©ã»ã«ã¼ãé¡ãªã©ãè½ã¨ãï¼ã 3299 // ð´ åç´ãªé¨åä¸è´ã ãã§ãé¤å¤èªããæ¸ãã¨å¿ ãç²ãéãããè²·åwiki ã® 3300 // ããã§ãç¨ãã£ã«ã é¢é£ã¯æ°ããªããã ['ãã§ã','instax','ãã£ã«ã '] ã¨æ¸ããçµæã 3301 // ãã§ã**æ¬ä½**ããåååã«ãå¯å£«ãã£ã«ã ããå«ãã ãã® X-T5 / X100VI ãæ°ããªã 3302 // æ±ãã«ãªã£ã¦ããï¼2026-08-16 çºè¦ãDB 宿¸¬ 995 ä»¶ä¸ 936 ä»¶ã誤é¤å¤ï¼ã 3303 // ãã®ãããèªã®åããæãã rules å´ã§æ¸ããããã«ãã¦ãã: 3304 // count_exclude_name_rules[] = { 3305 // strip: èªãæ¢ãåã«æ¶ãæååï¼ãã©ã³ãåãå¯å£«ãã¤ã«ã ãèªä½ã"ãã¤ã«ã "ãå«ãï¼ 3306 // all_of: [[åè£â¦], [åè£â¦]] â åã°ã«ã¼ããã 1 ã¤ä»¥ä¸å«ãæã ãè©²å½ 3307 // none_of: 1 ã¤ã§ãå«ãã ãé¤å¤ããªãï¼ã±ã¼ã¹ã»ã¢ã«ãã çã®å¨è¾ºå°ç©ã®ææ¸ï¼ 3308 // } 3309 // none_of 㯠strip åã®å ã®ååã§è¦ãï¼ãã©ã³ãåãæ¶ããå¯ä½ç¨ã§ææ¸èªã¾ã§ 3310 // æ¶ããã®ãé¿ããããï¼ã 3311 function splitCountItem(it, fs) { 3312 const name = (it.name || '').toLowerCase(); 3313 if (fs.count_exclude_categories && fs.count_exclude_categories.indexOf(it.category) !== -1) return false; 3314 if (fs.count_exclude_name_patterns && 3315 fs.count_exclude_name_patterns.some(p => name.includes(p.toLowerCase()))) return false; 3316 for (const r of (fs.count_exclude_name_rules || [])) { 3317 let s = name; 3318 for (const w of (r.strip || [])) s = s.split(w.toLowerCase()).join(' '); 3319 if (!(r.all_of || []).every(g => g.some(a => s.includes(a.toLowerCase())))) continue; 3320 if ((r.none_of || []).some(w => name.includes(w.toLowerCase()))) continue; 3321 return false; 3322 } 3323 return true; 3324 } 3325 3326 function splitCountable(items, fs) { 3327 return items.filter(it => splitCountItem(it, fs)); 3328 } 3329 3330 // countable ã¯ç®±ãã¨ã« 1 åã ãä½ã£ã¦ä½¿ãåããæ¡ä»¶ãã¨ã« filter ãã㨠3331 // 5,000 éã Ã ç®±æ° ã¶ãé åãä½ããã¨ã«ãªãã60ç¹è¦æ¨¡ã§ 1 ç§ç´ã«è¨ãã 3332 function splitCondOk(cond, countable, amount) { 3333 if (cond.any_of) return cond.any_of.some(c => splitCondOk(c, countable, amount)); 3334 if (cond.amount) return amount >= cond.amount.min; 3335 if (cond.count) { 3336 let n = 0; 3337 for (const it of countable) if (splitItemMatches(it, cond.count)) n += it.quantity; 3338 return n >= cond.count.min; 3339 } 3340 if (cond.weighted_count) { 3341 const w = cond.weighted_count.weights || {}; 3342 let sum = 0; 3343 for (const it of countable) sum += (w[it.category] || 0) * it.quantity; 3344 return sum >= cond.weighted_count.min; 3345 } 3346 return true; 3347 } 3348 3349 // 1 ç®±ã¶ãã®éæè² æ ã¨ããã®çç±ï¼ç»é¢ã«ãã®ã¾ã¾åºãæ¥æ¬èªï¼ãè¿ãã 3350 // ð´ costArg === null ã¯ãéæãè¨ç®ã«å ¥ãããã OFFããã®æã¯å®è²»ã ãã§ãªã 3351 // **åºå´ã®åºå®å·®å¼ï¼è²·åwiki 3,000åã»ã¢ãã¤ã«ããã¯ã¹ 2,000åï¼ãå¼ããªã**ã 3352 // 以å㯠OFF ããå®è²» 0 åãã§è¡¨ãã¦ãããããunmet.type='fixed' ã®åå²ã ãã 3353 // è¨å®ãããæãã¦æ®ãããã§ãã¯ãå¤ãã¦ããéæ âÂ¥3,000ããåºãä¸ã« 3354 // ãã®åºã®æåãã 3,000 åä½ãè©ä¾¡ããã¦å岿¡ã®é ä½ã¾ã§çã£ã¦ãã 3355 // ï¼2026-08-17 å©ç¨è å ±åï¼ãOFF ã null ã§è¡¨ãã¦ååå²ã®å ¥å£ã§è½ã¨ãã 3356 function splitShipping(storeKey, items, amount, costArg) { 3357 const rules = (window.KX_SHIPPING_RULES || {}).stores || {}; 3358 const r = rules[storeKey]; 3359 const cost = costArg !== undefined ? costArg : splitEffShipCost(); 3360 // éé¡ã¯ 0 ã«è½ã¨ãããæ¡ä»¶ãã®ãã®ï¼ãã¨ä½ç¹ã§ç¡æãï¼ã¯å¤æææã¨ãã¦åºãããã®ã§ 3361 // å¤å®ã¯æå¾ã¾ã§ç¶ãããç»é¢å´ã¯ off ãè¦ã¦ãè¨ç®ã«å«ã
3361ããã¨æ¸ã 3362 const off = cost === null; 3363 const paid = off ? 0 : cost; 3364 if (!r || !r.free_ship) return { cost: paid, off: off, label: 'éæã¯èªå·±è² æ ', free: false }; 3365 const fs = r.free_ship; 3366 if (fs.kind === 'always') return { cost: 0, off: off, label: 'æ¡ä»¶ãªãã§çæãOK', free: true, keepLabel: true }; 3367 if (fs.kind === 'never') return { cost: paid, off: off, label: 'çæãã«å¯¾å¿ãã¦ããªãããèªå·±è² æ ', free: false }; 3368 if (fs.kind === 'unknown') return { cost: paid, off: off, label: 'éææ¡ä»¶ãå ¬è¡¨ããã¦ããªãããèªå·±è² æ ã§è¨ç®', free: false }; 3369 3370 const countable = splitCountable(items, fs); 3371 const ok = (fs.all_of || []).every(c => splitCondOk(c, countable, amount)); 3372 if (ok) return { cost: 0, off: off, label: 'çæãã®æ¡ä»¶ãæºããããéæç¡æ', free: true, keepLabel: true }; 3373 3374 // ä½ãè¶³ããªãã®ããæ¥æ¬èªã§åºãããããå©ç¨è ã«ã¨ã£ã¦ä¸çªä½¿ããæ å ±ã«ãªãã 3375 // ããã¨ãã¯æé ã«ä¸åº¦ã ãç½®ãï¼åé ã«ä»ããã¨ããã¨3ç¹ã¨ãã¨2ä¸åãã¨éè¤ãã¦èªã¿ã«ããï¼ 3376 const lack = c => { 3377 if (c.amount) return (c.amount.min - amount).toLocaleString() + 'å'; 3378 if (c.count) { 3379 let n = 0; 3380 for (const it of countable) if (splitItemMatches(it, c.count)) n += it.quantity; 3381 // åä½ã¯å¸¸ã«ãç¹ãï¼2026-08-16 May æç¤ºï¼ãåºã®åæã¯ãã¹ãã3ç¹ãï¼wikiã» 3382 // ä¸ä¸ç®ã»ãã©ã´ã³ï¼ã¨ãã¹ãã3å°ãï¼ã¢ãã¤ã«ä¸çªã»è²·å奥ã»ã¢ãã¹ãã» 3383 // ã¢ãã¤ã«ããã¯ã¹ã»ã²ã¹ãï¼ãæ··å¨ãã¦ããã®ã§ãã©ã¡ããã«å¯ããªã㨠3384 // åãç»é¢ã§è¡¨è¨ãå²ããã以åã¯ãcategories ã«ã¹ãããå ¥ã£ã¦ããã°å°ã 3385 // ã¨ãã¦ããããæ£®æ£®ã®ããã« 8 ã«ãã´ãªãæ°ããæ¡ä»¶ã¾ã§ãå°ãã«ãªã£ã¦ããã 3386 // åä½ãæããã¨ã5ç¹ä»¥ä¸ or ã¹ãã3ç¹ä»¥ä¸ããããã¨3ç¹ã2ç¹ãã«ãªã£ã¦ 3387 // ã©ã¡ãã®æ¡ä»¶ãèªããªããªãã®ã§ãçµãè¾¼ã¿ã®ããå´ã ãæ¬å¼§ã§å¯¾è±¡ãæ·»ããã 3388 // 対象ãå¤ãããæï¼æ£®æ£®ã® 8 ã«ãã´ãªï¼ã¯æ¸ããªãï¼ãã ã®ãç¹ãã«æ»ãã 3389 const cs = c.count.categories || [], ps = c.count.include_name_patterns || []; 3390 const scope = ps.length ? ps.join('ã»') : (cs.length === 1 ? cs[0] : ''); 3391 return (c.count.min - n) + 'ç¹' + (scope ? `ï¼${scope}ï¼` : ''); 3392 } 3393 return null; 3394 }; 3395 const short = []; 3396 (fs.all_of || []).forEach(c => { 3397 if (splitCondOk(c, countable, amount)) return; 3398 if (c.any_of) { 3399 const cands = c.any_of.map(lack).filter(Boolean); 3400 if (cands.length) short.push(cands.join('ã')); 3401 } else { 3402 const t = lack(c); 3403 if (t) short.push(t); 3404 } 3405 }); 3406 const unmet = fs.unmet || { type: 'actual' }; 3407 const c = off ? 0 : (unmet.type === 'fixed' ? unmet.amount : cost); 3408 const label = short.length 3409 ? 'ãã¨' + short.join('ã¨') + 'ã§éæç¡æ' 3410 : 'çæãã®æ¡ä»¶ãæºãããªãããèªå·±è² æ '; 3411 // `unmet.extra_fee`ï¼ã²ã¹ãã¢ãã¤ã«ã®äºåææ°æ 1,000 åï¼ã¯**éé¡ã«è¶³ããªã**ã 3412 // åæã¯ãããããã¯ä»¥å¤ã®çæã/åå人æã㯠çæãéæï¼äºåææ°æ1,000åãå·®å¼ãï¼ 3413 // æãããã©ããã¯ééä¼ç¤¾ã®é¸ã³æ¹ã§æ±ºã¾ãããã¡ãã§ã¯åãããªãã夿°æ´¾ï¼ããããã¯ã» 3414 // å æãï¼ã«ã¯æãããªãã®ã§ãé»ã£ã¦å¼ãã¨ãã®åºãä¸å½ã«ä½ãè¦ãããæ¯è¾¼ææ°æã 3415 // éå ¬éã®åºã¨åãæ±ãã§ã**å¼ããã«æ³¨è¨ã ãåºã**ï¼2026-08-17 May è£æ±ºï¼ã 3416 const extra = unmet.extra_fee 3417 ? { amount: unmet.extra_fee, carriers: (r.free_ship.carriers || []).join('ã»') } : null; 3418 return { cost: c, off: off, label: label, free: false, extra: extra, 3419 fixed: !off && unmet.type === 'fixed', keepLabel: short.length > 0 }; 3420 } 3421 3422 function splitTransferFee(storeKey, items, amount) { 3423 if (!splitEffFeeOn()) return 0; 3424 const rules = (window.KX_SHIPPING_RULES || {}).stores || {}; 3425 const tf = (rules[storeKey] || {}).transfer_fee; 3426 // ð´ ãã«ã¼ã«ãç¡ããï¼ã0 åãã§ã¯ãªããåºãè¶³ããç´å¾ã¯å¿ ãããã«è½ã¡ã 3427 // ãã®åºã ãææ°æã¼ãæ±ãï¼æåããé大è©ä¾¡ãã¦ãåãã¦å£²ããæé©åã 3428 // ãã®åºãä¸å½ã«é¸ã¶ï¼2026-08-16 ã«æ°6åºã§çºçï¼ã 3429 // å®é¡ãåãããªã以ä¸ãåæãªæ¢å®å¤ãå¼ãã®ãåããããåãªã®ã§ 3430 // **å¼ããã« 0 ãè¿ãããã®ä»£ãã UI ã«ãæ¯è¾¼ææ°æã¯é
3430å ¬éãã¨åºã** 3431 // ï¼è¡¨ç¤ºã¯ split-box-meta å´ãæ°åãé»ã£ã¦ä½ããªããã¨ãåªå ããï¼ã 3432 if (!tf || tf.unknown) return 0; 3433 const qty = items.reduce((s, it) => s + it.quantity, 0); 3434 for (const t of (tf.tiers || [])) { 3435 if (t.when) { 3436 if (qty >= (t.when.count_min || 0) && amount >= (t.when.amount_min || 0)) return t.fee; 3437 } else if (t.amount_below !== undefined) { 3438 if (amount < t.amount_below) return t.fee; 3439 } else if (t.amount_at_least !== undefined) { 3440 if (amount >= t.amount_at_least) return t.fee; 3441 } 3442 } 3443 return tf.default || 0; 3444 } 3445 3446 // --- æé©åæ¬ä½ --- 3447 // 5,000 éãã®çµåããåãã®ã§ãåæä¸ã¯ãç®±ã®ä¸èº«ã®é åããä½ããªãã 3448 // æ¡ä»¶å¤å®ã«è¦ãæ°ï¼ç¹æ°ã»éé¡ã»ã«ãã´ãªå¥å°æ°ï¼ã¯äºåã« itemÃåº ã®ãã¹ã¯ã¸è½ã¨ãã¦ããã 3449 // åæä¸ã¯ãã¹ã¯ãè¦ã¦è¶³ãã ãã«ãããæç´°ãçµã¿ç«ã¦ãã®ã¯ä¸ä½ã«æ®ã£ãçµåãã ãã 3450 function splitPrepare(items, stores, priceOf) { 3451 const nI = items.length, nS = stores.length; 3452 const price = new Float64Array(nI * nS); 3453 const qty = new Float64Array(nI); 3454 items.forEach((it, i) => { 3455 qty[i] = it.quantity; 3456 stores.forEach((s, k) => { price[i * nS + k] = priceOf(it, s) || 0; }); 3457 }); 3458 3459 const rules = (window.KX_SHIPPING_RULES || {}).stores || {}; 3460 // åºãã¨ã«ããã®æ¡ä»¶ã«å½ã¦ã¯ã¾ãååã¯ã©ããããå ã«é åã§æã¤ 3461 const meta = stores.map((s) => { 3462 const fs = (rules[s] || {}).free_ship || { kind: 'never' }; 3463 // å¤å®ã¯ splitCountable ã¨åã 1 æ¬ï¼splitCountItemï¼ã«å¯ããã 3464 // ããã«åãæ¡ä»¶ãããä¸åº¦æ¸ãã¨ãçæ¹ã ãç´ããæã« 3465 // ãæç´°ã®ç¹æ°ãã¨ãæé©åã使ãç¹æ°ããéãã«é£ãéã 3466 const countable = items.map(it => splitCountItem(it, fs)); 3467 const leaves = []; // {kind:'count'|'weighted', mask|weights} 3468 const index = c => { 3469 if (c.any_of) return { any_of: c.any_of.map(index) }; 3470 if (c.amount) return { amount: c.amount }; 3471 if (c.count) { 3472 const mask = items.map((it, i) => countable[i] && splitItemMatches(it, c.count)); 3473 leaves.push({ mask: mask }); 3474 return { countIdx: leaves.length - 1, min: c.count.min, raw: c }; 3475 } 3476 if (c.weighted_count) { 3477 const w = c.weighted_count.weights || {}; 3478 const vals = items.map((it, i) => (countable[i] ? (w[it.category] || 0) : 0)); 3479 leaves.push({ vals: vals }); 3480 return { weightedIdx: leaves.length - 1, min: c.weighted_count.min, raw: c }; 3481 } 3482 return { always: true }; 3483 }; 3484 const tree = (fs.all_of || []).map(index); 3485 return { store: s, fs: fs, tree: tree, leaves: leaves, countable: countable, 3486 tf: (rules[s] || {}).transfer_fee }; 3487 }); 3488 return { nI, nS, price, qty, items, stores, meta }; 3489 } 3490 3491 function splitCondOkFast(node, sums, amount) { 3492 if (node.any_of) return node.any_of.some(n => splitCondOkFast(n, sums, amount)); 3493 if (node.amount) return amount >= node.amount.min; 3494 if (node.countIdx !== undefined) return sums[node.countIdx] >= node.min; 3495 if (node.weightedIdx !== undefined) return sums[node.weightedIdx] >= node.min; 3496 return true; 3497 } 3498 3499 function splitShipCostFast(m, sums, amount, totalQty, cost) { 3500 // cost === nullï¼ãéæãè¨ç®ã«å ¥ãããOF
3500Fãåºå®å·®å¼ãå«ãã¦ä¸åå¼ããªã 3501 // ï¼ãããç´ éããããã¨ãfixed ã®åºã ãæåããä½ãåºã¦é ä½ãçãï¼ 3502 if (cost === null) return 0; 3503 const fs = m.fs; 3504 if (fs.kind === 'always') return 0; 3505 if (fs.kind === 'never' || fs.kind === 'unknown') return cost; 3506 if (m.tree.every(n => splitCondOkFast(n, sums, amount))) return 0; 3507 const u = fs.unmet || { type: 'actual' }; 3508 return u.type === 'fixed' ? u.amount : cost; 3509 } 3510 3511 function splitFeeFast(m, amount, totalQty, on) { 3512 if (!on || !m.tf) return 0; 3513 for (const t of (m.tf.tiers || [])) { 3514 if (t.when) { 3515 if (totalQty >= (t.when.count_min || 0) && amount >= (t.when.amount_min || 0)) return t.fee; 3516 } else if (t.amount_below !== undefined) { 3517 if (amount < t.amount_below) return t.fee; 3518 } else if (t.amount_at_least !== undefined) { 3519 if (amount >= t.amount_at_least) return t.fee; 3520 } 3521 } 3522 return m.tf.default || 0; 3523 } 3524 3525 // çµåã 1 ã¤ã¶ãã®æåãããé åãä½ããã«æ°ã ãã§åºã 3526 function splitScore(prep, subset, cost, feeOn) { 3527 const { nS, price, qty, meta } = prep; 3528 const k = subset.length; 3529 const amount = new Float64Array(k), tq = new Float64Array(k); 3530 const sums = subset.map(si => new Float64Array(meta[si].leaves.length)); 3531 const used = new Uint8Array(k); 3532 let gross = 0; 3533 for (let i = 0; i < prep.nI; i++) { 3534 let bj = -1, bp = 0; 3535 const base = i * nS; 3536 for (let j = 0; j < k; j++) { 3537 const p = price[base + subset[j]]; 3538 if (p > bp) { bp = p; bj = j; } 3539 } 3540 if (bj < 0) continue; 3541 const q = qty[i]; 3542 gross += bp * q; 3543 amount[bj] += bp * q; tq[bj] += q; used[bj] = 1; 3544 const m = meta[subset[bj]], sv = sums[bj]; 3545 for (let L = 0; L < m.leaves.length; L++) { 3546 const lf = m.leaves[L]; 3547 if (lf.mask) { if (lf.mask[i]) sv[L] += q; } 3548 else sv[L] += lf.vals[i] * q; 3549 } 3550 } 3551 let ship = 0, fee = 0, boxes = 0; 3552 const usedStores = []; 3553 for (let j = 0; j < k; j++) { 3554 if (!used[j]) continue; 3555 boxes++; usedStores.push(subset[j]); 3556 ship += splitShipCostFast(meta[subset[j]], sums[j], amount[j], tq[j], cost); 3557 fee += splitFeeFast(meta[subset[j]], amount[j], tq[j], feeOn); 3558 } 3559 return { net: gross - ship - fee, gross, ship, fee, boxes, usedStores }; 3560 } 3561 3562 // ä¸ä½ topN ä»¶ã®çµåããè¿ãï¼å®éã«ååãå ¥ã£ãåºã®çµåãã§éè¤ãç³ãï¼ã 3563 // ãä¸çªå¾ãªåãæ¹ãã ãã ã¨ãç¹å®ã®åºã«éããããªãçã®äºæ ã«å¿ããããªãã®ã§åè£ãåºãã 3564 function splitRank(items, candidates, maxStores, priceOf) { 3565 const prep = splitPrepare(items, candidates, priceOf); 3566 // OFF 㯠0 åã§ã¯ãªã nullï¼ï¼è¨ç®ã«å ¥ããªãï¼ã0 ã«ããã¨åºå®å·®å¼ã®åºã ãæ®ãã 3567 // åºé æã¡è¾¼ã¿ã¢ã¼ãã§ã¯éæãæ¯è¾¼ææ°æãç¡ãï¼splitEff* ãé¢åãè¦ãï¼ 3568 const cost = splitEffShipCost(); 3569 const feeOn = splitEffFeeOn(); 3570 const seen = new Map(); 3571 const chosen = []; 3572 const walk = (start) => { 3573 if (chosen.length) { 3574 const r = splitScore(prep, chosen, cost, feeOn); 3575 if (r.boxes) { 3576 const key = r.usedStores.slice().sort().join('|'); 3577 const prev = seen.get(key); 3578 if (!prev || r.boxes < prev.boxes) seen.set(key, r); 3579 } 3580 } 3581 if (chosen.length >= maxStores) return; 3582 for (let i = start; i < candidates.length; i++) { 3583 chosen.push(i); walk(i + 1); chosen.pop(); 3584 } 3585 }; 3586 walk(0); 3587 const all = [...seen.values()].sort((a, b) => (b.net - a.net) || (a.boxes - b.boxes)); 3588 // ç»é¢ã«åºãã¶ãã ãããã¨ããç®±ã®ä¸èº«ãçµã¿ç«ã¦ã 3589 return all.map(r => ({ __prep: prep, __raw: r, net: r.net, gross: r.gross, 3590 ship: r.ship, fee: r.fee, boxCount: r.boxes, 3591 stores: r.usedStores.map(si => candidates[si]) })); 3592 } 3593 3594 // é¸ã°ããçµåãã ãæç´°ï¼ã©ã®ååãã©ã®åºã¸ï¼ãä½ã 3595 function splitDetail(entry) { 3596 const prep = entry.__prep; 3597 const subset = entry.__raw.usedStores; 3598 const groups = new Map(subset.map(si => [si, []]));
3599 prep.items.forEach((it, i) => { 3600 let bj = -1, bp = 0; 3601 for (const si of subset) { 3602 const p = prep.price[i * prep.nS + si]; 3603 if (p > bp) { bp = p; bj = si; } 3604 } 3605 if (bj >= 0) groups.get(bj).push({ it: it, unit: bp }); 3606 }); 3607 // é ä½è¨ç®ï¼splitRankï¼ã¨åãå¤ã渡ããOFF 㯠nullï¼è¨ç®ã«å ¥ããªã 3608 const cost = splitEffShipCost(); 3609 const boxes = []; 3610 for (const [si, list] of groups) { 3611 if (!list.length) continue; 3612 const its = list.map(x => x.it); 3613 const amount = list.reduce((s, x) => s + x.unit * x.it.quantity, 0); 3614 boxes.push({ 3615 store: prep.stores[si], items: its, list: list, amount: amount, 3616 ship: splitShipping(prep.stores[si], its, amount, cost), 3617 fee: splitFeeFast(prep.meta[si], amount, its.reduce((s, x) => s + x.quantity, 0), splitEffFeeOn()), 3618 }); 3619 } 3620 return boxes; 3621 } 3622 3623 // ä¸è¦§ã«åºãåºåã®ä¸¦ã³é ãæç´°ã®ç®±ï¼1ç®±ç®ã»2ç®±ç®â¦ï¼ã¨åããè²·åé¡ã®å¤ãé ãã«æããã 3624 // åæé ã®ã¾ã¾ã ã¨ã1çªç®ã«æ¸ãã¦ããåºããä¸è¦§ã¨æç´°ã§é£ãéããç §åã§ããªã 3625 function splitStoreOrder(entry) { 3626 return splitDetail(entry).sort((a, b) => b.amount - a.amount).map(b => b.store); 3627 } 3628 3629 function renderMatomeComparison(matomeItems, products, storeResults) { 3630 comparisonItems = matomeItems.map(item => ({ ...item })); 3631 comparisonProducts = products; 3632 comparisonModified = false; 3633 renderComparisonSummary(); 3634 renderBestFilter(); 3635 // æ¯è¼å¯¾è±¡åºãæ°éãå¤ãããå岿¡ãåºãç´ãï¼ä¸ã®ã«ã¼ãã¨é£ãéãããªãï¼ 3636 renderSplitPlan(); 3637 renderComparisonStoreCards(storeResults); 3638 } 3639 3640 function calculateStoreResults(matomeItems, products) { 3641 const productMap = {}; products.forEach(p => { if (p && p.jan) productMap[p.jan] = p; }); 3642 3643 // åååã®æé«è²·åä¾¡æ ¼ãäºåè¨ç®ï¼ãã£ã«ã¿å¯¾è±¡åºèã®ã¿ï¼ 3644 const bestPriceMap = {}; 3645 matomeItems.forEach(matomeItem => { 3646 const product = productMap[matomeItem.jan]; 3647 if (!product || !product.prices?.length) return; 3648 let bestPrice = 0, bestStore = ''; 3649 product.prices.forEach(p => { 3650 if (!bestFilterStores.has(p.store)) return; 3651 if (p.stale) return; // æ´æ°åæ¢/è²·å忢çãã®åºã¯æé«å¤ã®å¯¾è±¡å¤ï¼DP é ã¨ååºæºï¼ 3652 const dp = deobfuscatePrice(p.price) || 0; 3653 if (dp > bestPrice) { bestPrice = dp; bestStore = p.store; } 3654 }); 3655 bestPriceMap[matomeItem.jan] = { price: bestPrice, store: bestStore }; 3656 }); 3657 3658 const storeResults = {}; 3659 Object.keys(MATOME_STORE_NAMES).forEach(k => storeResults[k] = { name: MATOME_STORE_NAMES[k], total: 0, items: [], availableCount: 0, unavailableCount: 0, staleCount: 0 }); 3660 3661 matomeItems.forEach(matomeItem => { 3662 const product = productMap[matomeItem.jan]; 3663 if (!product) return; 3664 const best = bestPriceMap[matomeItem.jan] || { price: 0, store: '' }; 3665 Object.keys(MATOME_STORE_NAMES).forEach(storeKey => { 3666 const priceInfo = product.prices?.find(p => p.store === MATOME_STORE_NAMES[storeKey]); 3667 // stale = ä¾¡æ ¼æ´æ°ãæ¢ã¾ã£ã¦ããåºï¼è²·å忢/䏿¶ã®å¯è½æ§ï¼ã 3668 // DP é ã§ã¯ç°è²è¡¨ç¤º+æé«å¤å¯¾è±¡å¤ãªã®ã§ãã¾ã¨ãæ¯è¼ã§ãåè¨ã»æé«å¤ã«å«ããªã 3669 const isStale = !!(priceInfo && priceInfo.stale); 3670 const price = isStale ? 0 : (deobfuscatePrice(priceInfo?.price) || 0); 3671 const subtotal = price * matomeItem.quantity; 3672 storeResults[storeKey].items.push({ 3673 jan: matomeItem.jan, name: matomeItem.name, quantity: matomeItem.quantity, 3674 image: product.image_url || matomeItem.image_url || '', 3675 unitPrice: price, subtotal: subtotal, stale: isStale, 3676 bestPrice: best.price, bestStore: best.store, 3677 }); 3678 if (price > 0) { storeResults[storeKey].total += subtotal; storeResults[storeKey].availableCount += matomeItem.quantity; } 3679 else if (isStale) { storeResults[storeKey].staleCount += matomeItem.quantity; } 3680 else { storeResults[storeKey].unavailableCount += matomeItem.quantity; } 3681 }); 3682 }); 3683 // ãæ¯è¼å¯¾è±¡åºèãã§é¸æãããåºã®ã¿ã«ã¼ãè¡¨ç¤ºï¼æªé¸æåºã¯é表示ï¼ã 3684 // ããã§è¡¨ç¤ºã«ã¼ãã¨ãä»åºæé«ãåºæºãåãåºèéåã«ãªããæ´åããã 3685 // ð´ ããã¯**åºé ã¢ã¼ãï¼splitSettings.walkinï¼ãç¥ããªã**ââæ¥åºå ç®ãä¹ããã 3686 // æã¡è¾¼ããªãå® é å°éåºãè½ã¨ããªããéæã«ã¼ã«ãæãé㯠3687 // renderComparisonStoreCards() ãä½ãæããªãï¼legacy 表示㯠rules ã
3688 // èªããªãæã ãï¼ã®ã§å®å®³ã¯ç¡ãããlegacy ã復活ããããªã 3689 // renderSplitPlan ã¨åãåºæºï¼priceOf ã«æ¥åºå ç®ã»candidates ã walkin ã§ 3690 // çµãï¼ã¸æããªãã¨ãåãç»é¢ã«é£ãéããä»åºæé«ãã 2 系統åºãã 3691 return Object.values(storeResults) 3692 .filter(s => bestFilterStores.has(s.name)) 3693 .sort((a, b) => b.total - a.total); 3694 } 3695 3696 // åºèãããã¯å岿¡ããã«ã®æä½è¡ã®ä¸ã«åºãï¼ãã°ã«æ¬ä½ã¯ renderSplitPlan å´ï¼ã 3697 // å岿¡ãåºããªãç°å¢ã§ã¯å¾æ¥ã©ããåç¬ãããã¯ã«æã 3698 function renderBestFilter() { 3699 const allNamesSet = new Set(); 3700 comparisonProducts.forEach(p => { (p.prices || []).forEach(pr => { if (pr.store) allNamesSet.add(pr.store); }); }); 3701 const allNames = [...allNamesSet]; 3702 // åºé ã«æã¡è¾¼ãã¢ã¼ãã§ã¯ãåºé è²·åã®æ ç¹ãç¡ãåºï¼å® é å°éï¼ã¯æã¡è¾¼ããªãã 3703 // ããããæ¶ããã«ãæ¼ããªãï¼çç±ãã§æ®ãï¼**ãªãåè£ããæ¶ããã®ããèªãã** 3704 // ããã«ããããï¼é»ã£ã¦æ¶ãã¨åºãå¤ããè¦ããç¡ãã®ã«æ¸ã£ã¦è¦ããï¼ã 3705 const walkinOn = splitWalkinOn(); 3706 const keyByName = {}; 3707 Object.keys(MATOME_STORE_NAMES).forEach(k => { keyByName[MATOME_STORE_NAMES[k]] = k; }); 3708 const noWalkin = n => walkinOn && !splitCanWalkin(keyByName[n]); 3709 // ããã¹ã¦é¸æï¼è§£é¤ããæ¼ããåºã ããè¦ããæ¼ããªãåºãæ°ã«å ¥ãã㨠3710 // å ¨é¨é¸ãã§ã allChecked ã«ãªããããã¿ã³ãããã¹ã¦é¸æãã®ã¾ã¾åºã¾ã 3711 const pickable = allNames.filter(n => !noWalkin(n)); 3712 const allChecked = pickable.length > 0 && pickable.every(n => bestFilterStores.has(n)); 3713 const chips = ` 3714 <button type="button" class="store-chip is-all${allChecked ? ' is-on' : ''}" 3715 onclick="toggleAllBestFilter(${allChecked ? 'false' : 'true'})">${allChecked ? 'ãã¹ã¦è§£é¤' : 'ãã¹ã¦é¸æ'}</button> 3716 ${allNames.map(name => { 3717 const on = bestFilterStores.has(name); 3718 if (noWalkin(name)) { 3719 return `<button type="button" class="store-chip is-off is-nowalkin" disabled 3720 title="å® é è²·åã®ã¿ã®ãããæã¡è¾¼ããåºèãããã¾ãã">${escapeHtml(name)}</button>`; 3721 } 3722 return `<button type="button" class="store-chip${on ? '' : ' is-off'}" aria-pressed="${on}" 3723 onclick="toggleBestFilterStore('${escapeHtml(name)}', ${on ? 'false' : 'true'})">${escapeHtml(name)}</button>`; 3724 }).join('')}`; 3725 3726 const inPanel = document.getElementById('matomeBestFilterInner'); 3727 const standalone = document.getElementById('matomeBestFilter'); 3728 if (inPanel) { 3729 inPanel.innerHTML = chips; 3730 const cnt = document.getElementById('matomeStoresCount'); 3731 // 忝ã»ååã¨ããä»ã®æ¡ä»¶ã§å®éã«é¸ã¹ãåºãã§æ°ããã 3732 // åºé ã¢ã¼ãã§å® é å°éåºãåæ¯ã«æ®ãã¨ãå ¨é¨é¸ãã§ã 21ï¼23 ã®ã¾ã¾ã«ãªã 3733 if (cnt) cnt.textContent = `${pickable.filter(n => bestFilterStores.has(n)).length}ï¼${pickable.length}åº`; 3734 const tg = document.getElementById('matomeStoresToggle'); 3735 if (tg) tg.onclick = () => { 3736 splitSettings.storesCollapsed = !splitSettings.storesCollapsed; 3737 saveSplitSettings(); 3738 inPanel.hidden = splitSettings.storesCollapsed; 3739 tg.setAttribute('aria-expanded', String(!splitSettings.storesCollapsed)); 3740 }; 3741 if (standalone) standalone.style.display = 'none'; 3742 return; 3743 } 3744 if (!standalone) return; 3745 standalone.style.display = 'block'; 3746 standalone.innerHTML = `<div class="matome-best-filter-inner">${chips}</div>`; 3747 } 3748 3749 window.toggleAllBestFilter = function(checked) { 3750 if (checked) { 3751 const all = new Set();
3752 comparisonProducts.forEach(p => { (p.prices || []).forEach(pr => { if (pr.store) all.add(pr.store); }); }); 3753 bestFilterStores = all; 3754 } else bestFilterStores = new Set(); 3755 saveBestFilter(); renderBestFilter(); recalcAndRenderCards(); 3756 }; 3757 window.toggleBestFilterStore = function(name, checked) { 3758 if (checked) bestFilterStores.add(name); else bestFilterStores.delete(name); 3759 saveBestFilter(); renderBestFilter(); recalcAndRenderCards(); 3760 }; 3761 function recalcAndRenderCards() { 3762 const activeItems = comparisonItems.filter(i => i.quantity > 0); 3763 if (activeItems.length === 0) return; 3764 const storeResults = calculateStoreResults(activeItems, comparisonProducts); 3765 // æ¯è¼å¯¾è±¡åºãæ°éãå¤ãããå岿¡ãåºãç´ãã 3766 // ãããå¿ããã¨ãå¤ããåºãå岿¡ã«æ®ãç¶ãã¦ä¸ã®ã«ã¼ãã¨çç¾ãã 3767 renderSplitPlan(); 3768 renderComparisonStoreCards(storeResults); 3769 } 3770 3771 let comparisonItems = []; let comparisonProducts = []; let comparisonModified = false; 3772 3773 // é¸ã°ãã¦ããä»£æ¿æ¡ï¼0 = ä¸çªãå¾ãªçµã¿åããï¼ãåºèæ°ãè¨å®ãå¤ãããå é ã«æ»ã 3774 let splitPickedIdx = 0; 3775 // ä»£æ¿æ¡ãªã¹ããéãã¦ããããéé㯠details ã® toggle ã§æ¾ããæ¡ãé¸ãã ãéããã 3776 // ãé¸ãã æ¡ãå é 以å¤ãªãéããã¾ã¾ãã«ãã㨠2 ä»¶ç®ä»¥éã ãç³ã¾ããå£ãã¦è¦ãã 3777 let splitAltsOpen = false; 3778
3779 function renderSplitPlan() { 3780 const host = document.getElementById('matomeSplitPlan'); 3781 if (!host) return; 3782 const rules = (window.KX_SHIPPING_RULES || {}).stores; 3783 if (!rules) { host.style.display = 'none'; return; } 3784 3785 const items = comparisonItems.filter(i => i.quantity > 0).map(i => { 3786 const p = comparisonProducts.find(x => x && x.jan === i.jan) || {}; 3787 return { jan: i.jan, name: i.name, quantity: i.quantity, category: p.category || 'ãã®ä»', 3788 image: p.image_url || i.image_url || '', prices: p.prices || [] }; 3789 }); 3790 if (!items.length) { host.style.display = 'none'; return; } 3791 3792 const walkinMap = splitWalkinMap(); 3793 const walkin = splitWalkinOn(); 3794 // åºé ã«æã¡è¾¼ãã¢ã¼ãã§ã¯ãæã¡è¾¼ããåºï¼ï¼åºé è²·åã®æ ç¹ãããåºï¼ã ããåè£ã 3795 // å® é å°éã®åºããéæ0ãã§ä¸¦ã¹ãã¨ãå®éã«ã¯åãåããªãå¾ãè¦ãããã¨ã«ãªãã 3796 const candidates = Object.keys(MATOME_STORE_NAMES) 3797 .filter(k => bestFilterStores.has(MATOME_STORE_NAMES[k])) 3798 .filter(k => !walkin || walkinMap[k]); 3799 // ä¾¡æ ¼ã¯å ã«è¡¨ã«è½ã¨ãï¼æ¢åã«ã¼ãã¨åãåºæºï¼æ¯è¼å¯¾è±¡åºã®ã¿ã»stale é¤å¤ï¼ã 3800 // æ¯å find() ãã㨠60ç¹Ã5,000éãã§å¹ãã¦ããã 3801 // åºé ã¢ã¼ãã§ã¯æ¥åºå ç®ã**å価ã«ä¸ä¹ãããå¤**ãå ¥ããã以éã®æé©åã» 3802 // ãä»åºæé«ãã»æç´°ã¯å ¨é¨ãã®è¡¨ãè¦ãã®ã§ã1 ãæç´ãã°å ¨é¨ãããã 3803 const mat = new Map(); 3804 const bonusMat = new Map(); // jan -> {key: å®éã«ä¸ä¹ãããæ¥åºå ç®} 3805 const vagueMat = new Map(); // jan -> {key: true}ï¼æ¥åºå¢é¡ã¯ãããéé¡ãéå ¬é 3806 items.forEach(it => { 3807 const row = {}, brow = {}, vrow = {}; 3808 (it.prices || []).forEach(p => { 3809 if (p.stale || !bestFilterStores.has(p.store)) return; 3810 const base = deobfuscatePrice(p.price) || 0; 3811 let bonus = 0; 3812 if (walkin && base > 0 && walkinMap[p.key]) { 3813 bonus = Math.max(0, parseInt(p.walkin_bonus, 10) || 0); 3814 // éé¡ãæ¸ããã¦ããªãæ¥åºå¢é¡ï¼æ£®æ£®ãå¨åº«ç¶æ³ã«ããå¤åããªã©ï¼ã¯ 3815 // æ°åãä½ããã«æ³¨è¨ã ãåºããæ¯è¾¼ææ°æãéå ¬éã®åºã¨åãæ±ãã 3816 if (!bonus && KX_WALKIN_VAGUE_RE.test(p.mark_note || '')) vrow[p.key] = true; 3817 } 3818 row[p.key] = base + bonus; 3819 if (bonus) brow[p.key] = bonus; 3820 }); 3821 mat.set(it.jan, row); 3822 bonusMat.set(it.jan, brow); 3823 vagueMat.set(it.jan, vrow); 3824 }); 3825 const priceOf = (it, key) => mat.get(it.jan)[key] || 0; 3826 const bonusOf = (jan, key) => (bonusMat.get(jan) || {})[key] || 0; 3827 const vagueOf = (jan, key) => !!(vagueMat.get(jan) || {})[key]; 3828 const usable = candidates.filter(k => items.some(it => priceOf(it, k) > 0)); 3829 // æ¡ãä½ããªãæã®**çç±**ãåºé ãã£ã«ã¿ã®ãããªã®ããå©ç¨è ãæ¯è¼å¯¾è±¡åºèã 3830 // å¤ãåã£ãã®ãã§æ¡å ãå¤ãããmat 㯠walkin ã§çµãåã®é¸æåºãå ¨é¨æã£ã¦ãã 3831 // ã®ã§ãã鏿åºã«ã¯ä¾¡æ ¼ãå¨ãã®ã« usable ã空ãï¼åºé ãã£ã«ã¿ãåå ã¨åããã 3832 // ãããè¦ãªãã¨ããåºé ã«æã¡è¾¼ãããå¤ãã¨æ¯ã¹ããã¾ããã¨**å¤ãã¦ãç´ããªã 3833 // æä½**ãæ¡å ãããã¨ã«ãªãï¼å®æ¸¬ã§èª¤æ¡å ãã¦ããï¼ã 3834 const emptyCause = (walkin && Object.keys(MATOME_STORE_NAMES) 3835 .filter(k => bestFilterStores.has(MATOME_STORE_NAMES[k])) 3836 .some(k => items.some(it => (mat.get(it.jan)[k] || 0) > 0))) 3837 ? 'walkin' : 'filter'; 3838 // æ¯è¼å¯¾è±¡åºã®ä¾¡æ ¼ãéé ã«ä¸¦ã¹ã表ãå岿¡ã§é¸ã°ããªãã£ãåºã¨ã®å·®ãåºãããã 3839 // ð´ **1ä½ã ãã§ãªã並ã³å ¨ä½ãæã¤**ââ表示å´ã§ããã®ç®±ã®åºããé¤ãã¦ãã 3840 // ä¸ä½2ä»¶ãæ¡ãã®ã§ãèªåºãæé«å¤ã®æã§ããèªåºãé¤ããä»åºã®1ä½/2ä½ãã 3841 // æ£ããå¼ããã1ä½ã ããæã£ã¦ããé ã¯èªåºãæé«ã 㨠bp=èªåºå価ã«ãªãã 3842 // `> unit` ãå½ã«ãªã£ã¦ hint ãã¨æ¶ãã¦ããï¼çµæã¯åãã ãã2ä½ãåºã 3843 // ã«ã¯ä¸¦ã³ãè¦ãï¼ãåãåºã2åå ¥ããã¨ã¯ãªãï¼key ãã¨ã«1ä¾¡æ ¼ï¼ã 3844 const bestOf = new Map();
3845 items.forEach(it => { 3846 const row = mat.get(it.jan); 3847 bestOf.set(it.jan, candidates 3848 .map(k => ({ store: k, price: row[k] || 0 })) 3849 .filter(x => x.price > 0) 3850 .sort((a, b) => b.price - a.price)); 3851 }); 3852 if (!usable.length) { splitRenderEmpty(host, walkin, emptyCause); return; } 3853 3854 // 3åºã¾ã§ã®å ¨åæã 1 åã ãåãã1åº/2åº/3åº ã®æè¯ãä¸ä½20ä»¶ãããããåãåºãã 3855 // ãã¿ã³ãã¨ã«åæãç´ã㨠4 åã®æéããããï¼60ç¹è¦æ¨¡ã§ 1 ç§è¶ ï¼ 3856 const all = splitRank(items, usable, 3, priceOf); 3857 const perMax = {}; 3858 for (const n of [1, 2, 3]) perMax[n] = all.find(r => r.boxCount <= n) || null; 3859 // åºèæ°ãå¢ããã¦ãæåããå¢ããªããªãããã®ãã¿ã³ã¯æ¼ããªãããã 3860 // æ¼ãã¦ãã¾ãã¨ã3åºã«ããã®ã«ä½ãå¤ãããªãï¼å£ãã¦ãããã«è¦ãã 3861 const noGain = {}; 3862 for (const n of [2, 3]) { 3863 noGain[n] = !perMax[n] || !perMax[n - 1] || perMax[n].net <= perMax[n - 1].net + 0.5; 3864 } 3865 // ä¸éã¯ä¿åããã叿å¤ã®ã¾ã¾ãå®éã«ä½¿ãã®ã¯ãå¢ããç¯å²ã§ã®æå¤§ã 3866 let effMax = splitSettings.maxStores; 3867 while (effMax > 1 && noGain[effMax]) effMax--; 3868 const ranked = all.filter(r => r.boxCount <= effMax).slice(0, 20); 3869 if (!ranked.length) { splitRenderEmpty(host, walkin, emptyCause); return; } 3870 if (splitPickedIdx >= ranked.length) splitPickedIdx = 0; 3871 const cur = ranked[splitPickedIdx]; 3872 const best = ranked[0]; 3873 3874 const surveyed = (window.KX_SHIPPING_RULES || {}).surveyed_at || ''; 3875 // 表示ããæ¥ä»ã¯ããã®æ¹æ¡ã«åºã¦ããåºãæå¾ã«ç¢ºèªããæ¥ãã®ãã¡ä¸çªå¤ããã®ã 3876 // å ¨åºä¸å¾ã®æ¥ä»ãåºãã¨ããã®æã«åå¾ã§ããªãã£ãåºã¾ã§ç¢ºèªæ¸ã¿ã«è¦ãã¦ãã¾ã 3877 // åºé ã¢ã¼ãã§åºãã¹ãã¯éææ¡ä»¶ã®ç¢ºèªæ¥ã§ã¯ãªã**æ ç¹ã確èªããæ¥** 3878 const planChecked = cur.stores 3879 .map(k => (walkin 3880 ? (splitWalkinMap()[k] || {}).at 3881 : ((rules[k] || {}).free_ship || {}).checked_at) || surveyed) 3882 .filter(Boolean).sort(); 3883 // æ¥æ¬èªãã¼ã¸ã« ISO ã® 2026-08-13 ãåºã¦ããã®ã§åæè¡¨è¨ã«ç´ã 3884 const jpDate = d => { 3885 const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(d || ''); 3886 return m ? `${+m[1]}å¹´${+m[2]}æ${+m[3]}æ¥` : (d || ''); 3887 }; 3888 const shownDate = jpDate(planChecked.length ? planChecked[0] : surveyed); 3889 // ãã¤ãã¹ã¯ãÂ¥-820ãã§ãªããâÂ¥820ããä»ã®ç®æï¼éæ âÂ¥1,100ï¼ã¨æãã 3890 const yen = n => (n < 0 ? 'âÂ¥' : 'Â¥') + Math.abs(Math.round(n)).toLocaleString(); 3891 const curBoxes = splitDetail(cur); 3892 const nBox = curBoxes.length; 3893 3894 3895 // ç®±ã®ä¸èº«ã¯ä¸ã®åºèã«ã¼ãã¨åãã1è¡1ååãã®è¡¨ã§åºãã 3896 // åååãã«ã³ãã§ä¸¦ã¹ã㨠60 ç¹è¦æ¨¡ã§èªããªããªããã 3897 const boxes = curBoxes.slice().sort((a, b) => b.amount - a.amount).map((b, i) => { 3898 const nm = MATOME_STORE_NAMES[b.store] || b.store; 3899 const conf = ((rules[b.store] || {}).free_ship || {}).confidence; 3900 const qty = b.items.reduce((s, it) => s + it.quantity, 0); 3901 const rows = b.list.slice().sort((x, y) => y.unit * y.it.quantity - x.unit * x.it.quantity).map(({ it, unit }) => { 3902 const img = it.image
3903 ? `<img src="${escapeHtml(it.image)}" alt="" loading="lazy" width="48" height="48">` 3904 : '<span class="matome-img-ph" aria-hidden="true"></span>'; 3905 // ãä»åºãï¼ãã®ç®±ã«å ¥ããåºãé¤ããæ¯è¼å¯¾è±¡åºãèªåºãæ··ãã㨠3906 // ãä»åºæé«ããèªåèªèº«ãæããã¨ãããã®ã§å¿ ãè½ã¨ãã¦ããæ¡ãã 3907 const others = (bestOf.get(it.jan) || []).filter(x => x.store !== b.store); 3908 const bo = others[0] || { price: 0, store: '' }; 3909 const bo2 = others[1] || null; 3910 const storeNm = k => escapeHtml(MATOME_STORE_NAMES[k] || k); 3911 // ãã®ç®±ã®åºããé«ãè²·ãåºããããªãå·®é¡ãåºãï¼åå²ããªã鏿ã®å¤æææï¼ 3912 // SP ã§ã¯ãæé«ãã¨æ¬å¼§ã CSS ã§è½ã¨ã㦠2 è¡ã«å²ãã 3913 // ç½®ãå ´æã PC ã¯åååã®ä¸ãSP ã¯ã@¥⯠à nãã®ä¸ï¼åååå´ã 4 è¡ã«ãªã£ã¦èã伸ã³ãããï¼ 3914 // 1ä½ã 2ä½ ã¨åãããå²ããªã1åä½ãã«ãããå ã¾ãªã㨠PC ã§æãè¿ããæã« 3915 // åºåã®éä¸ï¼è²·åã½ã ãªï¼ã¨ï¼ã§åãããæãã®ã¯ 1ä½ ã¨ 2ä½ ã®éã ãã 3916 const hintInner = (bo.price > unit) 3917 ? `<span class="mibh-1st">ä»åº<span class="mibh-h">æé«</span> Â¥${bo.price.toLocaleString()}` 3918 + `<span class="mibh-store">${storeNm(bo.store)}</span></span>` : ''; 3919 // ð´ 2ä½ã¯ **PC ã ã**ï¼SP ã¯å¹ ãç¡ããæ¢ã« 2 è¡ã«å²ã£ã¦ããï¼ã 3920 // æ¡ä»¶ã¯1ä½ã¨æãã¦ãèªåºããé«ããæã®ã¿ï¼ãã® hint ã®å½¹ç®ã 3921 // ãåå²ããã°ãã£ã¨é«ã売ããå ãã®æç¤ºãªã®ã§ãèªåºããå®ãåºã 3922 // 並ã¹ã¦ã夿ææã«ãªããªãã 3923 // ð´ å é ã® `<wbr>` ã¯å¿ é ã1ä½ ã 2ä½ ã nowrap ãªã®ã§ã**両è ã®éã« 3924 // æ¹è¡æ©ä¼ãç¡ã㨠white-space:normal ã§ãæãã**ã親 td ã® 3925 // overflow:hidden ã« 2ä½ ã丸ãã¨é£ãããï¼å®æ¸¬ innerWidth 1331px ã§ 3926 // é«ã 17px ã®1è¡ã®ã¾ã¾å³ãåãã¦ããã<wbr> ãå ¥ãã㨠35px ã®2è¡ã« 3927 // ãªã£ã¦åã¾ãï¼ããã³ãã¬ã¼ãã `${hintInner}${hint2}` ã¨ééç¡ãã§ 3928 // é£çµããã®ã§ãç©ºç½æåã¯å ¥ããªãã 3929 const hint2 = (hintInner && bo2 && bo2.price > unit) 3930 ? `<wbr><span class="mibh-2nd">2ä½ Â¥${bo2.price.toLocaleString()}` 3931 + `<span class="mibh-store">${storeNm(bo2.store)}</span></span>` : ''; 3932 // 2ä½ãåºãªãè¡ï¼ä»åºã1ã¤ããèªåºãä¸åããªãï¼ã§ãä¸ä½2åºãã¨è¨ããªã 3933 const hintTitle = hint2 3934 ? 'ãã®ç®±ã®åºãé¤ããæ¯è¼å¯¾è±¡åºã®ãã¡ãèªåºããé«ãè²·ãä¸ä½2åº' 3935 : 'ãã®ç®±ã®åºãé¤ããæ¯è¼å¯¾è±¡åºã®ãã¡ãèªåºããé«ãè²·ãåº'; 3936 const hint = hintInner 3937 ? `<span class="matome-item-best-hint hint-pc" title="${hintTitle}">${hintInner}${hint2}</span>` : ''; 3938 const hintSp = hintInner ? `<span class="matome-item-best-hint hint-sp">${hintInner}</span>` : ''; 3939 const diff = (bo.price > unit) ? (bo.price - unit) * it.quantity : 0; 3940 // æ¥åºå ç®ã¯ãã©ãããå¢ãããããåãããªãã¨æ°åãä¿¡ããããªãã®ã§ã 3941 // å価ã®é£ã«å¿ ãåºãï¼unit ã«ã¯æ¢ã«å ç®å¾ã®é¡ãå ¥ã£ã¦ããï¼ 3942 const bn = bonusOf(it.jan, b.store); 3943 const bnTag = bn 3944 ? `<span class="msi-walkin" title="åºé ã«æã¡è¾¼ãã æã ãä»ãå ç®ï¼ååºã®åèæ¬ããï¼">
3944æ¥åº +Â¥${bn.toLocaleString()}</span>` 3945 : (vagueOf(it.jan, b.store) 3946 ? '<span class="msi-walkin is-vague" title="æ¥åºå¢é¡ã¯ããã¾ãããéé¡ãå ¬è¡¨ããã¦ããªãããè¨ç®ã«å«ãã¦ãã¾ãã">æ¥åºå¢é¡ããï¼æªè¨ä¸ï¼</span>' 3947 : ''); 3948 return ` 3949 <tr class="matome-store-item"> 3950 <td class="matome-store-item-img"><a href="/dp/${escapeHtml(it.jan)}" tabindex="-1" aria-hidden="true">${img}</a></td> 3951 <td class="matome-store-item-name"><a href="/dp/${escapeHtml(it.jan)}" style="text-decoration:none;color:inherit;">${escapeHtml(it.name)}</a>${hint}<button type="button" class="msi-jan matome-jan-copy-btn" onclick="copyToClipboard('${escapeHtml(it.jan)}','JANãã³ãã¼ãã¾ãã')" title="JANãã³ãã¼">${escapeHtml(it.jan)}</button></td> 3952 <td class="matome-store-item-copy-jan"><button type="button" class="matome-jan-copy-btn" onclick="copyToClipboard('${escapeHtml(it.jan)}','JANãã³ãã¼ãã¾ãã')" title="JANãã³ãã¼">${escapeHtml(it.jan)}</button></td> 3953 <td class="matome-store-item-qty"><span class="msi-unit">@Â¥${unit.toLocaleString()} à ${it.quantity}</span>${bnTag}${hintSp}</td> 3954 <td class="matome-store-item-price">Â¥${(unit * it.quantity).toLocaleString()}${diff > 0 ? `<span class="matome-item-diff">+Â¥${diff.toLocaleString()}</span>` : ''}</td> 3955 </tr>`; 3956 }).join(''); 3957 // ãéæãè¨ç®ã«å ¥ããããå¤ãã¦ããæã«åºãã¹ããªã®ã¯éé¡ã§ã¯ãªãã 3958 // âÂ¥0 ã§ã âÂ¥3,000 ã§ããªããè¨ç®ã«å«ãã¦ããªããã¨æ¸ãã 3959 // æ¡ä»¶ãã®ãã®ï¼ãã¨â¯ç¹ã§éæç¡æï¼ã¯å¤æææã«ãªãã®ã§æ®ãã 3960 // èªå·±è² æ ããã¬ãã®è¨ã訳ã ãè½ã¨ãï¼keepLabelï¼ 3961 const shipChip = walkin 3962 ? '<span class="split-ship is-walkin">åºé æã¡è¾¼ã¿</span>' 3963 : b.ship.free 3964 ? '<span class="split-ship is-free">éæç¡æ</span>' 3965 : b.ship.off 3966 ? '<span class="split-ship is-skip">éæ è¨ç®ã«å«ãã</span>' 3967 : `<span class="split-ship is-paid">éæ â${yen(b.ship.cost)}</span>`; 3968 const shipReason = walkin 3969 ? '<span class="split-reason">éæã»æ¯è¾¼ææ°æã¯æããã¾ãã</span>' 3970 : (b.ship.off && !b.ship.keepLabel) ? '' 3971 : `<span class="split-reason">${escapeHtml(b.ship.label)}</span>`; 3972 // ã©ãã¸æã¡è¾¼ãåºãªã®ããåãæåãã§ããç§èå3è»ãã¨ãæ±äº¬+大éª+ç¦å²¡ãã§ã¯ 3973 // ç¾å®å³ãå ¨ãéãã®ã§ãæ¡ãé¸ã¶åã«è¦ãã¦ããå¿ è¦ããã 3974 const wk = walkin ? (splitWalkinMap()[b.store] || null) : null; 3975 const walkinLoc = wk 3976 ? `<span class="split-reason" title="${escapeHtml((wk.names || []).join('ï¼'))}${ 3977 wk.n > (wk.names || []).length ? ' ã»ã' : ''}">${wk.n}åºè${ 3978 splitWalkinAreas(b.store) ? `ï¼${escapeHtml(splitWalkinAreas(b.store))}ï¼` : ''}</span>` 3979 : ''; 3980 // ãã®ç®±ã§ä¹ã£ãæ¥åºå ç®ã®åè¨ãå価ã®é£ã«ãåºãã¦ãããã 3981 // ããã®åºã«æã¡è¾¼ãã¨ç·é¡ããããå¢ããã®ããã¯ç®±ã®åä½ã§ããèªããªã 3982 const boxBonus = walkin 3983 ? b.list.reduce((t, { it }) => t + bonusOf(it.jan, b.store) * it.quantity, 0) : 0; 3984 const walkinBonusChip = boxBonus 3985 ? `<span class="split-walkin-bonus">æ¥åºå ç® +${yen(boxBonus)}</span>` : ''; 3986 // ééä¼ç¤¾æ¬¡ç¬¬ã§æããäºåææ°æãè¨ä¸ããªã代ããã«æ¡ä»¶ãã¨æ¸ã 3987 const shipExtra = b.ship.extra 3988 ? `<span class="split-reason">${b.ship.extra.carriers 3989 ? escapeHtml(b.ship.extra.carriers) + '以å¤ã®çæãã¯' : ''}äºåææ°æ Â¥${
3990 b.ship.extra.amount.toLocaleString()}ï¼æªè¨ä¸ï¼</span>` : ''; 3991 return ` 3992 <!-- ç®±ã¯ã1ä½ã»2ä½ãã§ã¯ãªãåã 1 æ¡ã® 1ç®±ç®ã»2ç®±ç®ãé ä½è²ï¼.first ã®ç·ï¼ã¯ä»ããªã --> 3993 <div class="matome-store-card split-plan-card"> 3994 <div class="matome-store-card-header"> 3995 <div class="matome-store-card-rank"> 3996 ${nBox > 1 ? `<span class="split-box-no" aria-hidden="true">${i + 1}</span>` : ''} 3997 <a href="/store/${escapeHtml(b.store)}?from=matome" class="matome-store-name-link">${escapeHtml(nm)}</a> 3998 <span class="split-box-qty">${nBox > 1 ? `${i + 1}ç®±ç®ã»` : ''}${qty}ç¹</span> 3999 </div> 4000 <div class="matome-store-total"> 4001 <div class="matome-store-total-label">${nBox > 1 ? 'ãã®ç®±ã®è²·åä¾¡æ ¼' : 'åè¨è²·åä¾¡æ ¼'}</div> 4002 <div class="matome-store-total-value">${yen(b.amount)}</div> 4003 </div> 4004 </div> 4005 <div class="split-box-meta"> 4006 ${shipChip} 4007 ${walkinBonusChip} 4008 ${walkinLoc} 4009 ${shipReason} 4010 ${walkin ? '' : shipExtra} 4011 ${(!walkin && b.fee) ? `<span class="split-fee">æ¯è¾¼ææ°æ â${yen(b.fee)}</span>` : ''} 4012 ${(!walkin && (((window.KX_SHIPPING_RULES || {}).stores || {})[b.store] || {}).transfer_fee?.unknown) 4013 ? '<span class="split-reason">æ¯è¾¼ææ°æã¯éå ¬éã®ããæªè¨ä¸</span>' : ''} 4014 ${walkin 4015 ? `<button type="button" class="split-warn kx-loc-open" data-store="${escapeHtml(b.store)}" 4016 aria-haspopup="dialog">${wk && wk.h ? 'åºèã»å¶æ¥æéãè¦ã' : 'åºèãè¦ã'}</button>` 4017 : `<a href="/store/${escapeHtml(b.store)}?from=matome" class="split-warn${(conf && conf !== 'high') ? ' is-vague' : ''}">${ 4018 conf && conf !== 'high' 4019 ? 'éææ¡ä»¶ãããã¾ããªããè¦ç¢ºèª' 4020 : 'çºéåã«éææ¡ä»¶ã確èª'}</a>`} 4021 </div> 4022 <div class="matome-store-items-wrap"><table class="matome-store-items-table">${rows}</table></div> 4023 </div>`; 4024 }).join(''); 4025 4026 const alts = ranked.map((r, i) => { 4027 const names = splitStoreOrder(r).map(k => escapeHtml(MATOME_STORE_NAMES[k] || k)).join('ï¼'); 4028 const diff = i === 0 ? '<span class="split-alt-diff split-alt-top">ä¸çªãå¾</span>' 4029 : `<span class="split-alt-diff">${yen(r.net - best.net)}</span>`; 4030 return `<li><button type="button" class="split-alt${i === splitPickedIdx ? ' active' : ''}" data-idx="${i}"> 4031 <span class="split-alt-rank">${i + 1}</span> 4032 <span class="split-alt-stores">${names}</span> 4033 <span class="split-alt-net">${yen(r.net)}</span>${diff} 4034 </button></li>`; 4035 }).join(''); 4036 4037 // é¸ãã æ¡ã§ä¹ã£ãæ¥åºå ç®ã®åè¨ããéµéãªããããã ã£ããããåãããªã㨠4038 // åºé ã¸è¡ã価å¤ã夿ã§ããªãã®ã§ãå ç®ã¶ãã¯å¿ ãåãã¦åºã 4039 const curBonus = walkin 4040 ? curBoxes.reduce((t, b) => t + b.list.reduce( 4041 (u, { it }) => u + bonusOf(it.jan, b.store) * it.quantity, 0), 0) : 0; 4042 const breakdown = walkin 4043 ? `è²·å <strong>${yen(cur.gross)}</strong>` 4044 + (curBonus ? `ï¼ãã¡æ¥åºå ç® +${yen(curBonus)}ï¼` : '') 4045 + '<span class="split-ms"> ï¼ åºé æã¡è¾¼ã¿ã®ããéæã»æ¯è¾¼ææ°æãªã</span>' 4046 : `è²·å ${yen(cur.gross)}` 4047 + (cur.ship ? ` â éæ ${yen(cur.ship)}` : '') 4048 + (cur.fee ? ` â æ¯è¾¼ææ°æ ${yen(cur.fee)}` : '') 4049 + ` ï¼ <strong>${yen(cur.net)}</strong>`; 4050 4051 host.style.display = ''; 4052 host.innerHTML = ` 4053 <div class="split-head"> 4054 <h3>ããã¤ã®åºã«åããã®ãä¸çªãå¾ã</h3> 4055 ${splitControlsHtml()} 4056 </div> 4057 <div class="split-seg" role="group" aria-label="åããåºèæ°"> 4058 ${[1, 2, 3].map(n => { 4059 const r = perMax[n]; 4060 const off = !r || noGain[n]; 4061 const why = noGain[n] ? ` title="${n}åºã«åãã¦ãæåãã¯å¢ãã¾ãã"` : ''; 4062 // éé¡ã¯å¿ ãã1åºã«ã¾ã¨ããå ´åããåºæºã«ãããåºæºãæ··ãã㨠4063 // ã3åºã® +⯠㯠1åºæ¯ãªã®ã 2åºæ¯ãªã®ãããèªããªããªãã®ã§ãæè¨ã«ãåºæºãæ¸ãã 4064 // æ¼ããªãåºèæ°ã¯ç©ºç½ã«ãããã©ãããå¢ããªãã®ããæ¸ã 4065 const base = perMax[1] ? perMax[1].net : 0; 4066 const up = (r && n > 1) ? r.net - base : 0; 4067 const flat = n > 1 && r && noGain[n]; 4068 const upTxt = (!r || n === 1) ? '' 4069 : flat ? (up > 0.5 ? `${n - 1}åºããå¢ãã¾ãã` : '1åºã¨åé¡') 4070 : `1åºãã +${yen(up)}`; 4071 return `<button type="button" class="split-seg-btn${effMax === n ? ' active' : ''}" data-max="${n}"${off ? ' disabled' : ''}${why}>
4072 <span class="split-seg-label">${n}åºã¾ã§</span> 4073 <span class="split-seg-sum">${r ? yen(r.gross) : 'â'}</span> 4074 <span class="split-seg-up${flat ? ' is-flat' : ''}">${upTxt}</span> 4075 </button>`; 4076 }).join('')} 4077 </div> 4078 <p class="split-seg-note">éé¡ã¯è²·åç·é¡${walkin ? 'ï¼æ¥åºå ç®ãå«ãï¼' : 'ï¼éæã»æ¯è¾¼ææ°æãå·®ãå¼ãåï¼'}ã§ãã${walkin ? 'è¤æ°åºã«åããå ´åã¯ããã¹ã¦ã®åºèã«æã¡è¾¼ãåæã§ãã' : ''}${noGain[2] || noGain[3] ? 'æåããå¢ããªãåºèæ°ã¯é¸ã¹ã¾ããã' : ''}</p> 4079 <details class="split-alts"${splitAltsOpen ? ' open' : ''}> 4080 <summary><svg class="split-alts-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>ã»ãã®${splitSettings.maxStores > 1 ? 'çµã¿åãã' : 'åº'}ãè¦ãï¼ä¸ä½${ranked.length}ä»¶ï¼</summary> 4081 <ol class="split-alt-list">${alts}</ol> 4082 </details> 4083 <div class="split-boxes">${boxes}</div> 4084 <p class="split-total">${breakdown}</p> 4085 <p class="split-note"> 4086 ${walkin 4087 ? `åºé è²·åã®åºèã¯${shownDate ? escapeHtml(shownDate) + 'ã«' : ''}<a href="/store">ååº</a>ã®å ¬å¼ãµã¤ãã§ç¢ºèªããå 容ã§ãã 4088 æ¥åºå ç®ã¯ååºã®è²·åä¾¡æ ¼è¡¨ã®åèæ¬ï¼ãæ¥åº+â¯åãçï¼ã«ãã¨ã¥ãç®å®ã§ãåºèã»å¨åº«ç¶æ³ã«ãã£ã¦å¤ãããã¨ãããã¾ãã
4089 åºèã«ãã£ã¦åãæ±ãååãå仿éãæ¬äººç¢ºèªã®æ¡ä»¶ãç°ãªãã¾ããå¶æ¥æéã¨ãããã¦ãæ¥åºåã«ååºã®å ¬å¼ãµã¤ãã§ã確èªãã ããã` 4090 : `éææ¡ä»¶ã¯${shownDate ? escapeHtml(shownDate) + 'ã«' : ''}<a href="/store">ååº</a>ã®å ¬å¼ãµã¤ãã§ç¢ºèªããå 容ã«ãã¨ã¥ãç®å®ã§ãï¼æ¯æè¦ç´ãã¦ãã¾ãï¼ã 4091 ãµã¤ãºä¸éã»è¤æ°åå£ã®è©°ãæ¹ã»æ²ç¸ï¼é¢å³¶ããã®çºéã¯è¨ç®ã«å«ãã¦ãã¾ããã 4092 çºéåã«ååºã®å ¬å¼ãµã¤ãã§ææ°ã®æ¡ä»¶ãã確èªãã ããã`}<br> 4093 æ¡ä»¶ã«èª¤ããè¦ã¤ããå ´åã¯<a href="/contact" target="_blank" rel="noopener">ãåãåãã</a>ãããé£çµ¡ããã ããã¨å©ããã¾ãã 4094 </p>`; 4095 4096 host.querySelectorAll('.split-seg-btn').forEach(b => b.addEventListener('click', () => { 4097 splitSettings.maxStores = parseInt(b.dataset.max, 10); 4098 splitPickedIdx = 0; 4099 saveSplitSettings(); renderSplitPlan(); 4100 })); 4101 const altsBox = host.querySelector('.split-alts'); 4102 if (altsBox) altsBox.addEventListener('toggle', () => { splitAltsOpen = altsBox.open; }); 4103 host.querySelectorAll('.split-alt').forEach(b => b.addEventListener('click', () => { 4104 splitPickedIdx = parseInt(b.dataset.idx, 10); 4105 // é¸ãã ãä¸è¦§ã¯ç³ãã§æç´°ãè¦ããï¼ä½ä»¶ç®ãé¸ãã§ãåãæåï¼ 4106 splitAltsOpen = false; 4107 renderSplitPlan(); 4108 // ä¸ã®ã»ãã®æ¡ãé¸ã¶ã¨ãç³ãã æåã«è¦åºããç»é¢ã®ä¸ã¸å¤ã㦠4109 // ãã©ãã«é£ãã ãåãããªããç¶æ ã«ãªããå¤ããã¨ãã ãè¦ããä½ç½®ã¸æ»ã 4110 const sm = host.querySelector('.split-alts > summary'); 4111 if (!sm) return; 4112 const bar = document.querySelector('.sticky-toolbar'); 4113 const offset = (bar ? bar.offsetHeight : 0) + 12; 4114 if (sm.getBoundingClientRect().top < offset) { 4115 window.scrollTo({ top: sm.getBoundingClientRect().top + window.pageYOffset - offset, behavior: 'smooth' }); 4116 } 4117 })); 4118 bindSplitControls(host); 4119 } 4120 4121 function renderComparisonSummary() { 4122 const activeItems = comparisonItems.filter(item => item.quantity > 0); 4123 const totalQty = activeItems.reduce((sum, item) => sum + item.quantity, 0); 4124 document.getElementById('matomeResultSummary').textContent = `${activeItems.length}ç¨®é¡ åè¨${totalQty}åãæ¯è¼ä¸`; 4125 // è¦åºãã»æä½ãã¼ã¯éç HTML å´ãããã§ä½ãç´ãã¨å ±æãã¿ã³ãæ¶ããã®ã§è§¦ããªã 4126 document.getElementById('matomeItemsCount').textContent = `${activeItems.length}種é¡`; 4127 document.getElementById('matomeRecompareBtn').disabled = !comparisonModified; 4128 4129 const productsBox = document.getElementById('matomeProductsSummary'); 4130 productsBox.hidden = !!splitSettings.itemsCollapsed; 4131 productsBox.innerHTML = ` 4132 <div class="matome-summary-items"> 4133 ${comparisonItems.map(item => { 4134 const _img = item.image_url || (comparisonProducts.find(p => p && p.jan === item.jan) || {}).image_url || ''; 4135 const _imgCell = _img 4136 ? `<img src="${escapeHtml(_img)}" alt="" loading="lazy" width="44" height="44">` 4137 : '<span class="matome-img-ph" aria-hidden="true"></span>'; 4138 return ` 4139 <div class="matome-summary-item ${item.quantity === 0 ? 'zero-qty' : ''}" data-jan="${escapeHtml(item.jan)}"> 4140 <a href="/dp/${escapeHtml(item.jan)}" class="matome-summary-item-img" tabindex="-1" aria-hidden="true">${_imgCell}</a> 4141 <div class="matome-summary-item-main"> 4142 <a href="/dp/${escapeHtml(item.jan)}" class="matome-summary-item-name" style="text-decoration:none;color:inherit;">${escapeHtml(item.name)}</a> 4143 <!-- SP ã§ã¯ãã®è¡ã® JAN ãåºãï¼å³å´ã®ãã¿ã³ã¯å¹ ãç¡ãé ãã¦ããï¼ -->
4144 <span class="kx-jan-copy sp-jan" data-jan="${escapeHtml(item.jan)}" role="button" tabindex="0" data-copied-text="ã³ãã¼ãã¾ãã" title="ã¿ããã§JANãã³ãã¼">JAN ${escapeHtml(item.jan)}</span> 4145 </div> 4146 <button type="button" class="matome-summary-item-copy-jan matome-jan-copy-btn" onclick="copyToClipboard('${escapeHtml(item.jan)}','JANãã³ãã¼ãã¾ãã')" title="JANãã³ãã¼">${escapeHtml(item.jan)}</button> 4147 <div class="matome-summary-item-qty"> 4148 <button class="comparison-qty-minus">-</button> 4149 <input type="number" class="comparison-qty-input" value="${item.quantity}" min="0" max="999"> 4150 <button class="comparison-qty-plus">+</button> 4151 </div> 4152 <button class="matome-summary-item-delete">â</button> 4153 </div> 4154 `; 4155 }).join('')} 4156 </div> 4157 `; 4158 4159 document.querySelectorAll('.matome-summary-item').forEach(el => { 4160 const jan = el.dataset.jan; 4161 const input = el.querySelector('.comparison-qty-input'); 4162 el.querySelector('.comparison-qty-minus').addEventListener('click', () => { input.value = Math.max(0, parseInt(input.value) - 1); updateComparisonItemQty(jan, parseInt(input.value)); }); 4163 el.querySelector('.comparison-qty-plus').addEventListener('click', () => { input.value = Math.min(999, parseInt(input.value) + 1); updateComparisonItemQty(jan, parseInt(input.value)); }); 4164 input.addEventListener('change', () => { input.value = Math.max(0, Math.min(999, parseInt(input.value) || 0)); updateComparisonItemQty(jan, parseInt(input.value)); }); 4165 el.querySelector('.matome-summary-item-delete').addEventListener('click', () => { 4166 comparisonItems = comparisonItems.filter(i => i.jan !== jan); comparisonModified = true; renderComparisonSummary(); 4167 }); 4168 }); 4169 // 対象ååã®ééã60ç¹è¦æ¨¡ã ã¨ä¸è¦§ãç»é¢ãå é ããã®ã§ç³ããããã«ããã 4170 // ééç¶æ 㯠localStorage ã¨ãã°ã¤ã³ä¸ã®ã¢ã«ã¦ã³ãè¨å®ã®ä¸¡æ¹ã«æ®ã 4171 bindMatomeAddPanel(); 4172 // ãã¼ã®ãã¿ã³ã¯éçè¦ç´ ãåæç»ã®ãã³ addEventListener ããã¨ãã³ãã©ãç©ã¿ä¸ããã®ã§ onclick ã§ç½®ãæãã 4173 const itemsToggle = document.getElementById('matomeItemsToggle'); 4174 itemsToggle.setAttribute('aria-expanded', String(!splitSettings.itemsCollapsed)); 4175 itemsToggle.onclick = () => { 4176 const willOpen = !!splitSettings.itemsCollapsed; 4177 if (willOpen) toggleMatomeAddPanel(false); // 追å ããã«ã¨ã¯æä» 4178 setMatomeItemsOpen(willOpen, true); 4179 }; 4180 document.getElementById('matomeAddMoreBtn').onclick = (() => { 4181 toggleMatomeAddPanel(); 4182 }); 4183 document.getElementById('matomeRecompareBtn').onclick = (() => { recompareMatome(); }); 4184 } 4185 4186 // 追å ããååã¯ã¾ã ä¾¡æ ¼ãæã£ã¦ããªãã®ã§ã忝è¼ã®åã«è¶³ããªãåã ãåãã«è¡ã 4187 async function recompareMatome() { 4188 const activeItems = comparisonItems.filter(item => item.quantity > 0); 4189 if (activeItems.length === 0) return alert('æ¯è¼ããååãããã¾ãã'); 4190 saveMatomeToStorage({ items: activeItems, updatedAt: Date.now() }); 4191 document.getElementById('matomeStoreResults').innerHTML = ''; 4192 showLoading(true); 4193 try { 4194 const missing = activeItems.filter(it => !comparisonProducts.some(p => p && p.jan === it.jan)); 4195 if (missing.length) { 4196 const got = await fetchProductsBatch(missing.map(it => it.jan)); 4197 comparisonProducts = comparisonProducts.concat(got.filter(Boolean)); 4198 } 4199 const storeResults = calculateStoreResults(activeItems, comparisonProducts); 4200 comparisonModified = false;
4201 renderComparisonSummary(); 4202 renderComparisonStoreCards(storeResults); 4203 renderBestFilter(); 4204 renderSplitPlan(); 4205 } catch (err) { 4206 showError(err.message || '忝è¼ã«å¤±æãã¾ãã'); 4207 } finally { showLoading(false); } 4208 } 4209 4210 // ==================== ã¾ã¨ãæ¯è¼ã®ã追å ãããã« ==================== 4211 // æ¤ç´¢ãã¼ã¸ã¸é£ã°ãæ§å®è£ ã¯ãæ¯è¼çµæãæ¶ããã»å©çãã£ã³ã¹ã®å¸¯ãåºãã» 4212 // åè£ãæ¼ãã¨ååãã¼ã¸ã¸é£ã¶ï¼ï¼è¿½å ã«ãªããªãï¼ã® 3 ç¹ã§ä½¿ããªãã£ãã 4213 // è¨ç®ã«å ¥ãã/å ¥ããªãã¯è²ã ãã§ç¤ºããã**è¨å·ã®å½¢**ã§ç¤ºãï¼è²è¦ã«ä¾åãããªãï¼ã 4214 // ð´ 以å㯠ON/OFF ã©ã¡ãã§ãåãâãæãã¦ãããå®éã®å·®ã¯è²ã ãã ã£ãï¼æ³¨è¨ã®æå³ã 4215 // æãããã¦ããªãã£ãï¼2026-08-17 May æç¤ºã§ä¿®æ£ï¼ãONï¼æ ã«ãã§ãã¯ãOFFï¼ç©ºã®æ ã 4216 // ãã§ãã¯ããã¯ã¹ã®è¦ãç®ãã®ã¾ã¾ãªã®ã§ãè²ãè¦ããªãã¦ãç¶æ ãèªããã 4217 const splitCheckIco = (on) => '<svg class="split-ctl-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' 4218 + 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' 4219 + '<rect x="3" y="3" width="18" height="18" rx="4"></rect>' 4220 + (on ? '<polyline points="16.8 8.8 10.5 15.2 7.2 12"></polyline>' : '') 4221 + '</svg>'; 4222 4223 // åºèãã£ã«ã¿ã»3 ã¤ã®è¨ç®ãã°ã«ã»éæé¡ã¯åããæ¡ä»¶ãæ±ºãããæä½ãªã®ã§ã 4224 // è¦ãç®ã 1 ã¤ã®é¨åç³»ï¼.split-ctl-btnï¼ã«æã㦠1 è¡ã«ã¾ã¨ããã 4225 // å岿¡ã 1 ã¤ãä½ããªãã£ãæï¼åºé ã«æã¡è¾¼ããåºã«ä¾¡æ ¼ãç¡ãçï¼ã«ã 4226 // ãã®è¡ã ãã¯åºãï¼**è¨å®ãæ»ãææ®µãæ¶ããªã**ããã«ãããã颿°ã«åãåºãã¦ããã 4227 function splitControlsHtml() { 4228 // æ ç¹ãã¼ã¿ã 1 ä»¶ãèªãã¦ããªãæã¯ã¢ã¼ããæç«ããªãï¼å ¨åºã 4229 // ãæã¡è¾¼ããªããã«ãªãï¼ãæ¼ãã¦ã空ã®çµæããåºããªããã¿ã³ã¯åºããªã 4230 const hasWalkinData = Object.keys(splitWalkinMap()).length > 0; 4231 const walkin = splitWalkinOn(); 4232 // åºé ã§ã¯éæãæ¯è¾¼ææ°æãæãããªãããã°ã«ã®ä¿åå¤ã¯å¤ããã«ã 4233 // 表示ä¸ã¯ãè¨ç®ã«å«ãã¦ããªãã宿 ã©ãã OFFï¼æ¼ããªãç¶æ ã«ãã 4234 const feeOn = splitEffFeeOn(); 4235 const shipOn = !walkin && splitSettings.shipping; 4236 const lockTip = 'åºé ã«æã¡è¾¼ãå ´åã¯æããã¾ãã'; 4237 return ` 4238 <div class="split-controls"> 4239 <button type="button" class="split-ctl-btn" id="matomeStoresToggle" 4240 aria-expanded="${!splitSettings.storesCollapsed}" aria-controls="matomeBestFilterInner"> 4241 <svg class="split-ctl-ico split-ctl-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg> 4242 <span>æ¯è¼å¯¾è±¡åºè</span><span class="split-ctl-count" id="matomeStoresCount"></span> 4243 </button> 4244 <!-- ãããã¯æ¼ãããã¿ã³ã®çä¸ã«åºãããã°ã«ãæãã¨ä½ãéããã®ãåãããªã --> 4245 <div class="matome-best-filter-inner" id="matomeBestFilterInner"${splitSettings.storesCollapsed ? ' hidden' : ''}></div> 4246 ${!hasWalkinData ? '' : ` 4247 <button type="button" class="split-ctl-btn${walkin ? ' is-on' : ''}" id="splitWalkinChk" 4248 aria-pressed="${walkin}" title="ãèªèº«ã§åºèã«æã¡è¾¼ãåæã§è¨ç®ãã¾ããéæã»æ¯è¾¼ææ°æã¯æããããæ¥åºå ç®ã®ããååã¯å ç®å¾ã®ä¾¡æ ¼ã§æ¯ã¹ã¾ãã"> 4249 ${splitCheckIco(walkin)}<span>åºé ã«æã¡è¾¼ã</span> 4250 </button>`} 4251 <button type="button" class="split-ctl-btn${feeOn ? ' is-on' : ''}" id="splitFeeChk" 4252 aria-pressed="${feeOn}"${walkin ? ` disabled title="${lockTip}"` : ''}> 4253 ${splitCheckIco(feeOn)}<span>æ¯è¾¼ææ°æ<span class="sc-tail">ãè¨ç®ã«å ¥ãã</span></span> 4254 </button> 4255 <button type="button" class="split-ctl-btn${shipOn ? ' is-on' : ''}" id="splitShipChk" 4256 aria-pressed="${shipOn}"${walkin ? ` disabled title="${lockTip}"` : ''}> 4257 ${splitCheckIco(shipOn)}<span>éæ<span class="sc-tail">ãè¨ç®ã«å ¥ãã</span></span> 4258 </button> 4259 <label class="split-ctl-btn split-cost${shipOn ? '' : ' is-off'}" title="${walkin ? lockTip 4260 : (splitSettings.shipping 4261 ? 'çæãã®æ¡ä»¶ãæºãããªãç®±ã§ãèªå·±è² æ ã«ãªãéæã§ã' 4262 : 'ãéæãããªã³ã«ããã¨å ¥åã§ãã¾ã')}">
4263 <span class="split-cost-label">éæã®èªå·±è² æ é¡<span class="sc-tail">ï¼1ç®±ãããï¼</span></span> 4264 <span class="split-cost-field"><span class="split-cost-yen">Â¥</span><input type="number" id="splitShipCost" inputmode="numeric" value="${splitSettings.shippingCost}" min="0" max="9999" step="100" ${shipOn ? '' : 'disabled'}></span> 4265 </label> 4266 </div>`; 4267 } 4268 4269 // ===== åºèã»å¶æ¥æéã® popup ===== 4270 // æ¯è¼ã®éä¸ã§ /shops ã¸é£ã°ãã¨ããã£ããåºããæ¡ãç»é¢ããæ¶ããã 4271 // 使ã»é»è©±ã»å¶æ¥æéãå ¨ãã¼ã¸ã®ã¤ã³ã©ã¤ã³ script ã«è¼ããã«ã¯éãã®ã§ 4272 // ï¼37 åºã¶ããæ ç¹åã¨ã¨ãªã¢ã ãã®ä»ã§ã 3.8KBï¼ãæ¼ããåºã ã 4273 // /api/shop-locations ããåãã«è¡ã£ã¦ç³ãã§æã¤ã 4274 // ð´ <dialog>.showModal() ã使ãï¼top layer ã«åºãã®ã§ã親ãä½ãéãåãã 4275 // æèï¼.main{position:relative;z-index:1}ï¼ã«æ½ããªããbody ç´ä¸ã« 1 åã ãã 4276 const _kxLocCache = new Map(); 4277 let _kxLocSeq = 0; 4278 function kxLocDialog() { 4279 let d = document.getElementById('kxLocDialog'); 4280 if (d) return d; 4281 d = document.createElement('dialog'); 4282 d.id = 'kxLocDialog'; 4283 d.className = 'kx-loc-dialog'; 4284 d.setAttribute('aria-labelledby', 'kxLocTitle'); 4285 d.innerHTML = ` 4286 <div class="kx-loc-head"> 4287 <h3 id="kxLocTitle"></h3> 4288 <button type="button" class="kx-loc-close" aria-label="éãã"> 4289 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> 4290 </button> 4291 </div> 4292 <div class="kx-loc-body"></div> 4293 <div class="kx-loc-foot"></div>`; 4294 document.body.appendChild(d); 4295 d.querySelector('.kx-loc-close').addEventListener('click', () => d.close()); 4296 // èæ¯ï¼::backdropï¼ãæ¼ãããéãããã¯ãªãã¯ã¯ dialog èªèº«ãåããã®ã§ã 4297 // ä¸èº«ã®ç©å½¢ã®å¤ãã©ããã§å¤å®ãã 4298 d.addEventListener('click', (e) => { 4299 if (e.target !== d) return; 4300 const r = d.getBoundingClientRect(); 4301 if (e.clientX < r.left || e.clientX > r.right || 4302 e.clientY < r.top || e.clientY > r.bottom) d.close(); 4303 }); 4304 return d; 4305 } 4306 4307 async function openStoreLocations(storeKey) { 4308 const d = kxLocDialog(); 4309 const nm = MATOME_STORE_NAMES[storeKey] || storeKey; 4310 const shopLink = `<a href="/store/${escapeHtml(storeKey)}?from=matome">${escapeHtml(nm)}ã®åºèãã¼ã¸</a>`; 4311 d.querySelector('#kxLocTitle').textContent = `${nm}ã®åºè`; 4312 let data = _kxLocCache.get(storeKey); 4313 // ä¸åº¦è¦ãåºã¯åãã«è¡ããªãã®ã§ãèªã¿è¾¼ã¿ä¸â¦ããæã¾ãªã 4314 // ï¼åºãã¨ãéããç¬éã«ä¸èº«ãå ¥ãæ¿ããã¡ãã¤ãã«ãªãï¼ 4315 if (!data) { 4316 d.querySelector('.kx-loc-body').innerHTML = '<p class="kx-loc-msg">èªã¿è¾¼ã¿ä¸â¦</p>'; 4317 d.querySelector('.kx-loc-foot').innerHTML = ''; 4318 } 4319 if (typeof d.showModal === 'function') { if (!d.open) d.showModal(); } 4320 else d.setAttribute('open', ''); 4321 4322 // ð´ å¿çãè¿ãã¾ã§ã«å¥ã®åºãéããã¦ããå¯è½æ§ãããã**åºå㯠Bã»ä½æã¯ A** 4323 // ã®ç»é¢ãä½ãã¨ããã®ã¾ã¾éãåºã¸è¡ããããã¨ã«ãªããéããã³çªå·ãé²ãã¦ã 4324 // èªåãææ°ã§ãªããã°æããã«éããï¼`d.open` ã ãã§ã¯éããæããé²ããªãï¼ã 4325 const seq = ++_kxLocSeq; 4326 if (!data) { 4327 try { 4328 const r = await fetch('/api/shop-locations/' + encodeURIComponent(storeKey), 4329 { credentials: 'same-origin' }); 4330 if (!r.ok) throw new Error('http ' + r.status); 4331 data = await r.json(); 4332 _kxLocCache.set(storeKey, data); 4333 } catch (e) { 4334 if (seq !== _kxLocSeq) return;
4335 // åããªãã£ãæã«ç©ºã®æ ã ãè¦ããªããåºèãã¼ã¸ã«ã¯å¿ ãè¼ã£ã¦ãã 4336 d.querySelector('.kx-loc-body').innerHTML = 4337 `<p class="kx-loc-msg">åºèæ å ±ãåå¾ã§ãã¾ããã§ããã${shopLink}ã§ã確èªãã ããã</p>`; 4338 return; 4339 } 4340 } 4341 if (!d.open || seq !== _kxLocSeq) return; // éããããï¼å¥ã®åºã«åãæ¿ãã£ã 4342 4343 const list = (data.stores || []).map(st => { 4344 const meta = []; 4345 // ð´ ç¡ãæã«ä½ãåºããªãã¨ãè¦è½ã¨ãããã®ããå ããç¡ããã®ããèªããªãã 4346 // ä½æã®æ±ãï¼ä½æã¯å ¬å¼ãµã¤ãã§å ¬éããã¦ãã¾ããï¼ã¨æãã 4347 if (!st.hours) meta.push('<span class="kx-loc-meta-item is-muted">å¶æ¥æéã®è¨è¼ãªã</span>'); 4348 if (st.hours) meta.push(`<span class="kx-loc-meta-item"> 4349 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><polyline points="12 7 12 12 15.5 14"></polyline></svg> 4350 ${escapeHtml(st.hours)}</span>`); 4351 if (st.tel) meta.push(`<a class="kx-loc-meta-item kx-loc-tel" href="tel:${escapeHtml(String(st.tel).replace(/-/g, ''))}"> 4352 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1.9.3 1.8.6 2.7a2 2 0 0 1-.4 2.1L8 9.8a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.5 2.7.6a2 2 0 0 1 1.7 2z"></path></svg> 4353 ${escapeHtml(st.tel)}</a>`); 4354 return `<li class="kx-loc-item"> 4355 <div class="kx-loc-name">${escapeHtml(st.name)}</div> 4356 <div class="kx-loc-addr${st.address ? '' : ' is-muted'}">${st.address 4357 ? escapeHtml(st.address) : '使ã¯å ¬å¼ãµã¤ãã§å ¬éããã¦ãã¾ãã'}</div> 4358 ${meta.length ? `<div class="kx-loc-meta">${meta.join('')}</div>` : ''} 4359 ${st.note ? `<div class="kx-loc-note">${escapeHtml(st.note)}</div>` : ''} 4360 ${st.map_url ? `<a class="kx-loc-map" href="${escapeHtml(st.map_url)}" target="_blank" 4361 rel="noopener nofollow noreferrer" referrerpolicy="no-referrer" 4362 aria-label="${escapeHtml(st.name)} ã Google ãããã§éã"> 4363 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 6-9 13-9 13s-9-7-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg> 4364 Google ãããã§éã</a>` : ''} 4365 </li>`; 4366 }).join(''); 4367 4368 d.querySelector('.kx-loc-body').innerHTML = list 4369 ? `<ul class="kx-loc-list">${list}</ul>` 4370 : '<p class="kx-loc-msg">åºé è²·åã®åºèãç»é²ããã¦ãã¾ããã</p>'; 4371 const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(data.surveyed_at || ''); 4372 const day = m ? `${+m[1]}å¹´${+m[2]}æ${+m[3]}æ¥` : ''; 4373 d.querySelector('.kx-loc-foot').innerHTML = 4374 (data.note ? `<p class="kx-loc-fnote">æ ç¹ã«ã¤ãã¦: ${escapeHtml(data.note)}</p>` : '') 4375 + `<p class="kx-loc-fnote">${day ? escapeHtml(day) + 'ã«' : ''}ååºã®å ¬å¼ãµã¤ãã§ç¢ºèªããå 容ã§ãã` 4376 + `å¶æ¥æéã使ã¯å¤æ´ããããã¨ããããããæ¥åºåã«${shopLink}ã¨å ¬å¼ãµã¤ãã§ã確èªãã ããã</p>`; 4377 } 4378 4379 // æ¡ã 1 ã¤ãä½ããªãã£ãæã**ããã«ãã¨æ¶ãã¦ã¯ãããªã**ââåºé ã¢ã¼ã㯠4380 // åè£åºãçµãã®ã§ãæ¶ãã¨ãåºé ã«æã¡è¾¼ãããæ»ããã¿ã³ã¾ã§ä¸ç·ã«æ¶ãã¦
4381 // ç»é¢ã空ã®ã¾ã¾è©°ãï¼è¨å®ã¯ä¿åãããã®ã§ãªãã¼ããã¦ãç´ããªãï¼ã 4382 function splitRenderEmpty(host, walkin, cause) { 4383 if (!walkin) { host.style.display = 'none'; return; } 4384 host.style.display = ''; 4385 // ð´ æ¡å ããæä½ã¯ãå®éã«ããã°ç´ãæä½ã§ãªããã°ãªããªãã 4386 // åºãå¤ãåã£ãã ãã®äººã«ãåºé ã«æã¡è¾¼ããå¤ããã¨è¨ã£ã¦ãä½ãèµ·ããªãã 4387 const msg = cause === 'walkin' 4388 ? `åºé ã«æã¡è¾¼ããåºèã§ã¯ããã®ååã®è²·åä¾¡æ ¼ãè¦ã¤ããã¾ããã§ããã 4389 ãåºé ã«æã¡è¾¼ãããå¤ãã¨ãå® é è²·åã®ã¿ã®åºèãå«ãã¦æ¯ã¹ããã¾ãã` 4390 : `æ¯è¼å¯¾è±¡åºèãé¸ã°ãã¦ããªãããé¸ãã åºèã«ãã®ååã®è²·åä¾¡æ ¼ãããã¾ããã 4391 ä¸ã®ãæ¯è¼å¯¾è±¡åºèãããé¸ã³ç´ãã¦ãã ããã`; 4392 host.innerHTML = ` 4393 <div class="split-head"> 4394 <h3>ããã¤ã®åºã«åããã®ãä¸çªãå¾ã</h3> 4395 ${splitControlsHtml()} 4396 </div> 4397 <p class="split-empty">${msg}</p>`; 4398 bindSplitControls(host); 4399 } 4400 4401 // æä½è¡ã®ãã³ãã©ãæããå ´æï¼éå¸¸ã®æ¡ï¼æ¡ãä½ããªãã£ãæã®ãç¥ããï¼ã 4402 // ã©ã¡ãã§ãåãé ç·ã«ãªããã 1 æ¬ã«ã¾ã¨ãã 4403 function bindSplitControls(host) { 4404 const walkinBtn = host.querySelector('#splitWalkinChk'); 4405 if (walkinBtn) walkinBtn.addEventListener('click', () => { 4406 splitSettings.walkin = !splitSettings.walkin; 4407 splitPickedIdx = 0; saveSplitSettings(); renderSplitPlan(); 4408 }); 4409 const chk = host.querySelector('#splitShipChk'); 4410 if (chk && !chk.disabled) chk.addEventListener('click', () => { 4411 splitSettings.shipping = !splitSettings.shipping; splitPickedIdx = 0; saveSplitSettings(); renderSplitPlan(); 4412 }); 4413 const cost = host.querySelector('#splitShipCost'); 4414 if (cost) cost.addEventListener('change', () => { 4415 const v = parseInt(cost.value, 10); 4416 splitSettings.shippingCost = isNaN(v) ? SPLIT_DEFAULTS.shippingCost : Math.max(0, Math.min(9999, v)); 4417 splitPickedIdx = 0; saveSplitSettings(); renderSplitPlan(); 4418 }); 4419 const fee = host.querySelector('#splitFeeChk'); 4420 if (fee && !fee.disabled) fee.addEventListener('click', () => { 4421 splitSettings.transferFee = !splitSettings.transferFee; splitPickedIdx = 0; saveSplitSettings(); renderSplitPlan(); 4422 }); 4423 // ãæ¥åºåã«å¶æ¥æéã»åºèã確èªã㯠/shops ã¸é£ã°ãã popup ã§åºã 4424 // ï¼æ¯è¼ã®éä¸ã§é·ç§»ããã¨ãåºããã°ããã®æ¡ãç»é¢ããæ¶ããï¼ 4425 host.querySelectorAll('.kx-loc-open').forEach(b => b.addEventListener('click', 4426 () => openStoreLocations(b.dataset.store))); 4427 // åºèãããã¯ãã®ããã«ã®ä¸ã«ç½®ãã®ã§ãããã«ãæãçµãã¦ããæµãè¾¼ã 4428 renderBestFilter(); 4429 } 4430 4431 let _mapTimer = null, _mapSeq = 0; 4432 4433 // 対象ååä¸è¦§ã®ééãèªåã§éããå ´å㯠persist ããªãï¼ã¦ã¼ã¶ã¼ãé¸ãã ç¶æ ã§ã¯ãªãããï¼ 4434 function setMatomeItemsOpen(open, persist) { 4435 const box = document.getElementById('matomeProductsSummary'); 4436 const btn = document.getElementById('matomeItemsToggle'); 4437 if (!box || !btn) return; 4438 splitSettings.itemsCollapsed = !open; 4439 box.hidden = !open; 4440 btn.setAttribute('aria-expanded', String(open)); 4441 if (persist) saveSplitSettings(); 4442 } 4443 4444 function toggleMatomeAddPanel(open) { 4445 const panel = document.getElementById('matomeAddPanel'); 4446 const btn = document.getElementById('matomeAddMoreBtn'); 4447 if (!panel || !btn) return; 4448 const next = open === undefined ? panel.hidden : open; 4449 panel.hidden = !next; 4450 btn.setAttribute('aria-expanded', String(next)); 4451 btn.classList.toggle('is-on', next); 4452 if (next) { 4453 // 追å ããã«ã¨å¯¾è±¡ååä¸è¦§ã¯åæã«éã㨠SP ã§ç»é¢ãåã¾ããçæ¹ã ãéã 4454 setMatomeItemsOpen(false); 4455 const input = document.getElementById('matomeAddInput'); 4456 input.value = ''; 4457 document.getElementById('matomeAddResults').innerHTML = 4458 '<p class="map-hint">åååã»JANã»ASIN ãå ¥åããã¨åè£ã表示ããã¾ããé¸ã¶ã¨ãã®æ¯è¼ã«
4458追å ããã¾ãã</p>'; 4459 setTimeout(() => input.focus(), 30); 4460 } 4461 } 4462 4463 function renderMatomeAddResults(list, query) { 4464 const host = document.getElementById('matomeAddResults'); 4465 if (!list.length) { 4466 host.innerHTML = `<p class="map-hint">ã${escapeHtml(query)}ãã«ä¸è´ããååã¯è¦ã¤ããã¾ããã§ããã</p>`; 4467 return; 4468 } 4469 host.innerHTML = '<ul class="map-list">' + list.map(p => { 4470 const added = comparisonItems.some(i => i.jan === p.jan && i.quantity > 0); 4471 const img = p.image_url 4472 ? `<img src="${escapeHtml(p.image_url)}" alt="" loading="lazy" width="40" height="40">` 4473 : '<span class="map-ph" aria-hidden="true"></span>'; 4474 return `<li> 4475 <button type="button" class="map-row${added ? ' is-added' : ''}" data-jan="${escapeHtml(p.jan)}" 4476 data-name="${escapeHtml(p.name || p.jan)}" data-img="${escapeHtml(p.image_url || '')}" 4477 ${added ? 'disabled' : ''}> 4478 <span class="map-thumb">${img}</span> 4479 <span class="map-body"> 4480 <span class="map-name">${escapeHtml(p.name || p.jan)}</span> 4481 <span class="map-jan">${escapeHtml(p.jan)}</span> 4482 </span> 4483 <span class="map-act">${added ? 'è¿½å æ¸ã¿' : '追å '}</span> 4484 </button> 4485 </li>`; 4486 }).join('') + '</ul>'; 4487 host.querySelectorAll('.map-row:not([disabled])').forEach(row => { 4488 row.addEventListener('click', () => { 4489 const { jan, name, img } = row.dataset; 4490 const exist = comparisonItems.find(i => i.jan === jan); 4491 if (exist) exist.quantity = Math.min(999, (exist.quantity || 0) + 1); 4492 else comparisonItems.push({ jan, name, quantity: 1, image_url: img }); 4493 addToMatome(jan, name, 1, img); 4494 comparisonModified = true; 4495 renderComparisonSummary(); 4496 row.classList.add('is-added'); 4497 row.disabled = true; 4498 row.querySelector('.map-act').textContent = 'è¿½å æ¸ã¿'; 4499 showCopiedToast(`ã${name}ãã追å ãã¾ãããã忝è¼ããæ¼ãã¨ä¾¡æ ¼ãåãè¾¼ã¿ã¾ã`); 4500 }); 4501 }); 4502 } 4503 4504 function bindMatomeAddPanel() { 4505 const input = document.getElementById('matomeAddInput'); 4506 const close = document.getElementById('matomeAddClose'); 4507 if (!input || input.dataset.bound) return; 4508 input.dataset.bound = '1'; 4509 close.addEventListener('click', () => toggleMatomeAddPanel(false)); 4510 input.addEventListener('keydown', e => { if (e.key === 'Escape') toggleMatomeAddPanel(false); }); 4511 input.addEventListener('input', () => { 4512 const q = input.value.trim(); 4513 clearTimeout(_mapTimer); 4514 const host = document.getElementById('matomeAddResults'); 4515 if (q.length < 2) { 4516 host.innerHTML = '<p class="map-hint">2æå以ä¸å ¥åãã¦ãã ããã</p>'; 4517 return; 4518 } 4519 const seq = ++_mapSeq; 4520 _mapTimer = setTimeout(async () => { 4521 host.innerHTML = '<p class="map-hint">æ¤ç´¢ä¸â¦</p>'; 4522 try { 4523 const data = await apiCall(`/api/search?q=${encodeURIComponent(q)}&limit=20&offset=0`); 4524 if (seq !== _mapSeq) return; // æã¡ç´ãããåã¯æ¨ã¦ã 4525 renderMatomeAddResults((data && data.results) || [], q); 4526 } catch (err) { 4527 if (seq === _mapSeq) host.innerHTML = '<p class="map-hint">æ¤ç´¢ã«å¤±æãã¾ãããæéãããã¦å度ã試ããã ããã</p>'; 4528 } 4529 }, 250); 4530 }); 4531 } 4532 4533 4534 function updateComparisonItemQty(jan, quantity) { 4535 const item = comparisonItems.find(i => i.jan === jan); 4536 if (item) { 4537 item.quantity = quantity; comparisonModified = true; 4538 const el = document.querySelector(`.matome-summary-item[data-jan="${jan}"]`); 4539 if (el) quantity === 0 ? el.classList.add('zero-qty') : el.classList.remove('zero-qty'); 4540 document.getElementById('matomeRecompareBtn').disabled = false;
4541 const activeItems = comparisonItems.filter(i => i.quantity > 0); 4542 document.getElementById('matomeResultSummary').textContent = `${activeItems.length}ç¨®é¡ åè¨${activeItems.reduce((sum, i) => sum + i.quantity, 0)}åãæ¯è¼ä¸`; 4543 document.getElementById('matomeItemsCount').textContent = `${activeItems.length}種é¡`; 4544 } 4545 } 4546 4547 // å ¨åºã®ã«ã¼ãã縦ã«ä¸¦ã¹ãã¨ãä¸ã®å岿¡ã¨åãæ å ±ã 31 åç¹°ãè¿ããã¦èªã¿åããªãã 4548 // ãã©ã®åºã«åºãããã¯å岿¡ããã«å´ï¼1åºã¾ã§ï¼ååºã®åç¬æ¡ãé ä½ã§é¸ã¹ãï¼ã«ä¸æ¬åããã 4549 function renderComparisonStoreCards(storeResults) { 4550 const host = document.getElementById('matomeStoreResults'); 4551 if (host) host.innerHTML = ''; 4552 if (window.KX_SHIPPING_RULES && window.KX_SHIPPING_RULES.stores) return; 4553 renderComparisonStoreCardsLegacy(storeResults); 4554 } 4555 4556 // éæã«ã¼ã«ãèªããå岿¡ããã«ãåºããªãæã ã使ã徿¥è¡¨ç¤º 4557 function renderComparisonStoreCardsLegacy(storeResults) { 4558 const storeUrls = Object.assign({ 'è²·åä¸ä¸ç®': 'https://www.1-chome.com/', 'è²·åååº': 'https://www.kaitorishouten-co.jp/', '森森買å': 'https://www.morimori-kaitori.jp/', 'è²·åã«ãã¤': 'https://kaitori-rudeya.com/', 'è²·åwiki': 'https://kaitori.wiki/', 'ã¢ãã¤ã«ä¸çª': 'https://www.mobile-ichiban.com/', 'å®¶é»å¸å ´': 'https://www.kaden-ichiba.com/', 'è²·åãã ã©': 'https://kaitori-homura.com/', 'ããè²·å': 'https://hachiden.com/', 'è²·åããããå': 'https://www.kaitorikensakukun.com/' }, window.KX_STORE_URLS || {}); 4559 document.getElementById('matomeStoreResults').innerHTML = storeResults.map((store, index) => { 4560 const isFirst = index === 0; 4561 let bestTotal = 0; 4562 store.items.forEach(item => { 4563 if (item.quantity > 0 && item.bestPrice > 0) bestTotal += item.bestPrice * item.quantity; 4564 }); 4565 const extraIfBest = bestTotal - store.total; 4566 return ` 4567 <div class="matome-store-card ${isFirst ? 'first' : ''}"> 4568 <div class="matome-store-card-header"> 4569 <div class="matome-store-card-rank"> 4570 <span class="matome-store-rank-badge">${isFirst ? '1' : (index + 1)}</span> 4571 <a href="${storeUrls[store.name] || '#'}" target="_blank" ${kxStoreLinkAttrs(storeUrls[store.name] || '#')} class="matome-store-name-link">${escapeHtml(store.name)}</a> 4572 </div> 4573 <div class="matome-store-total"> 4574 <div class="matome-store-total-label">åè¨è²·åä¾¡æ ¼</div> 4575 <div class="matome-store-total-value">Â¥${store.total.toLocaleString()}</div> 4576 ${extraIfBest > 0 ? `<div class="matome-store-best-extra">ååå¥ã«æé«å¤ã§ +Â¥${extraIfBest.toLocaleString()}</div>` : ''} 4577 </div> 4578 </div> 4579 ${(store.unavailableCount > 0 || store.staleCount > 0) ? `<div class="matome-store-warning"> 4580 <svg class="ui-icon" style="width:14px;height:14px;display:inline;vertical-align:middle;margin-right:4px;" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> 4581 ${[store.unavailableCount > 0 ? `${store.unavailableCount}åã¯è²·åä¸å¯` : '', store.staleCount > 0 ? `${store.staleCount}åã¯è²·å忢ä¸ã®å¯è½æ§ï¼åè¨ã«å«ããï¼` : ''].filter(Boolean).join('ã»')} 4582 </div>` : ''} 4583 <table class="matome-store-items-table"> 4584 ${store.items.filter(item => item.quantity > 0).map(item => { 4585 const isBest = item.unitPrice > 0 && item.unitPrice >= item.bestPrice; 4586 const diff = (item.unitPrice > 0 && !isBest && item.bestPrice > 0) ? (item.bestPrice - item.unitPrice) * item.quantity : 0; 4587 const bestHint = (item.unitPrice > 0 && !isBest && item.bestPrice > 0) 4588 ? `<span class="matome-item-best-hint">
4588ä»åºæé« Â¥${item.bestPrice.toLocaleString()} (${escapeHtml(item.bestStore)})</span>` 4589 : ''; 4590 const imgCell = item.image 4591 ? `<img src="${escapeHtml(item.image)}" alt="" loading="lazy" width="48" height="48">` 4592 : '<span class="matome-img-ph" aria-hidden="true"></span>'; 4593 return ` 4594 <tr class="matome-store-item"> 4595 <td class="matome-store-item-img"><a href="/dp/${escapeHtml(item.jan)}" tabindex="-1" aria-hidden="true">${imgCell}</a></td> 4596 <td class="matome-store-item-name"><a href="/dp/${escapeHtml(item.jan)}" style="text-decoration:none;color:inherit;">${escapeHtml(item.name)}</a>${bestHint}</td> 4597 <td class="matome-store-item-copy-jan"><button type="button" class="matome-jan-copy-btn" onclick="copyToClipboard('${escapeHtml(item.jan)}','JANãã³ãã¼ãã¾ãã')" title="JANãã³ãã¼">${escapeHtml(item.jan)}</button></td> 4598 <td class="matome-store-item-qty">${item.unitPrice > 0 ? `@Â¥${item.unitPrice.toLocaleString()} à ${item.quantity}` : `à ${item.quantity}`}</td> 4599 <td class="matome-store-item-price ${item.unitPrice > 0 ? '' : 'unavailable'}">${item.unitPrice > 0 ? `Â¥${item.subtotal.toLocaleString()}` : (item.stale ? 'è²·å忢ä¸?' : 'è²·åä¸å¯')}${diff > 0 ? `<span class="matome-item-diff">+Â¥${diff.toLocaleString()}</span>` : ''}</td> 4600 </tr>`; 4601 }).join('')} 4602 </table> 4603 ${extraIfBest > 0 ? `<div class="matome-store-extra-summary"><span>ååå¥ã«æé«å¤ã§å£²å´ããå ´å</span><span class="matome-store-extra-amount">+Â¥${extraIfBest.toLocaleString()}</span></div>` : ''} 4604 </div> 4605 `; 4606 }).join(''); 4607 } 4608 4609 // ã¾ã¨ãæ¯è¼ä¸ã¯ãè²·åXã¨ã¯ï¼ã以ä¸ã®ç´¹ä»ãç³ã¿ãã³ãã¥ããã£å°ç·ã ãæ®ãã 4610 // å ã® display ãè¦ãã¦ããã¦æ»ãï¼æ±ºãæã¡ã§ '' ã«ããã¨å¥ã®æå®ãå£ãï¼ 4611 function toggleAboutForMatome(hide) { 4612 const about = document.getElementById('aboutSection'); 4613 if (!about) return; 4614 // æ®ãã®ã¯å ¨ãã¼ã¸å ±éã® CTAï¼_cta.htmlï¼{{CTA}} ã§æ³¨å ¥ããã .cta-wrapï¼ã ãã 4615 // ããã§ç¬èªã® SNS ãããã¯ãæ¸ãã¨å ¨ç«ã¨è¦ãç®ãããã 4616 const keep = about.querySelector('.cta-wrap'); 4617 const keepTop = keep ? [...about.children].find(c => c.contains(keep)) : null; 4618 [...about.children].forEach(el => { 4619 if (hide) { 4620 if (el === keepTop) return; 4621 if (el.dataset.matomeHidden === undefined) el.dataset.matomeHidden = el.style.display; 4622 el.style.display = 'none'; 4623 } else if (el.dataset.matomeHidden !== undefined) { 4624 el.style.display = el.dataset.matomeHidden; 4625 delete el.dataset.matomeHidden; 4626 } 4627 }); 4628 } 4629 4630 function goBackFromMatomeComparison() { 4631 document.getElementById('matomeResultSection').style.display = 'none'; 4632 // ã¾ã¨ã FAB ãè§ã®å¸ããæ¬æ¥ã®ä½ç½®ï¼å¸ +68pxï¼ã¸æ»ããå¤ãå¿ãã㨠4633 // ãããã«æ»ã£ãæã« DC/X FAB ã¨åãå¸ã§éãªã 4634 document.body.classList.remove('kx-matome-compare'); 4635 toggleMatomeAddPanel(false); 4636 // é ãã¦ãããå¤åããã人æ°ã¡ãã¥ã¼ãã»ã¯ã·ã§ã³ã復帰 4637 const kxQuickSection = document.querySelector('.kx-quick-section'); 4638 if (kxQuickSection) kxQuickSection.style.display = ''; 4639 toggleAboutForMatome(false); 4640 const heroWrap = document.querySelector('.hero-wrap'); 4641 if (heroWrap) heroWrap.style.display = ''; 4642 document.querySelectorAll('.discord-fab, .x-fab').forEach(el => { el.style.display = ''; }); 4643 // URLããmatome=compareãåé¤ 4644 const u = new URL(location.href); u.searchParams.delete('matome'); 4645 history.replaceState(null, '', u.search ? u.toString() : u.pathname); 4646 // ããããã¸ãï¼ã©ãããæ¯è¼ã«å ¥ã£ã¦ããããã«æ»ãï¼May æç¤º 2026-08-14ï¼ã 4647 // ç´åãååãã¼ã¸ã§ãããã«ã¯æ»ããªã 4648 elements.productSection.classList.remove('active'); 4649 elements.searchSection.style.display = 'block'; 4650 window.scrollTo({ top: 0, behavior: 'smooth' }); 4651 updateMatomeUI(); previousViewState = null; 4652 } 4653 4654 // ==================== ãæ°ã«å ¥ãæ©è½ (Favorites) ==================== 4655 function getFavoritesFromStorage() { 4656 try { 4657 const data = localStorage.getItem(FAVORITES_KEY); 4658 return data ? JSON.parse(data) : { items: [], displayMode: 'normal', updatedAt: null }; 4659 } catch (err) { return { items: [], displayMode: 'normal', updatedAt: null }; } 4660 } 4661 4662 function saveFavoritesToStorage(favs) { favs.updatedAt = Date.now(); localStorage.setItem(FAVORITES_KEY, JSON.stringify(favs)); } 4663 4664 function isFavorite(jan) { return getFavoritesFromStorage().items.some(item => item.jan === jan); } 4665
4666 function updateFavoriteStarUI(jan) { 4667 const starBtn = document.getElementById('favoriteStarBtn'); 4668 if (!starBtn) return; 4669 const isFav = isFavorite(jan); 4670 starBtn.querySelector('.star-empty').style.display = isFav ? 'none' : 'inline'; 4671 starBtn.querySelector('.star-filled').style.display = isFav ? 'inline' : 'none'; 4672 } 4673 4674 function toggleFavorite() { 4675 if (!currentProductData || !currentJan) return; 4676 let favs = getFavoritesFromStorage(); 4677 const wasAdded = !isFavorite(currentJan); 4678 if (!wasAdded) favs.items = favs.items.filter(item => item.jan !== currentJan); 4679 else if (favs.items.length < MAX_FAVORITES) favs.items.push({ jan: currentJan, name: currentProductData.name, image_url: currentProductData.image_url, addedAt: Date.now() }); 4680 else { alert(`æå¤§${MAX_FAVORITES}ä»¶ã¾ã§ã§ã`); return; } 4681 4682 saveFavoritesToStorage(favs); updateFavoriteStarUI(currentJan); 4683 // ãµã¼ãã¼åæï¼ãã°ã¤ã³æ¸ã¿ã®å ´åï¼ 4684 try { 4685 fetch('/api/favorites', { 4686 method: wasAdded ? 'POST' : 'DELETE', 4687 headers: {'Content-Type': 'application/json'}, 4688 body: JSON.stringify({jan: currentJan}) 4689 }).catch(()=>{}); 4690 } catch(e) {} 4691 } 4692 4693 // ==================== äºä»¶ç»å®ä¸åå§å ==================== 4694 function bindEvents() { 4695 elements.searchInput.addEventListener('input', () => { syncEnterHint(); debouncedSearch(); }); 4696 elements.searchInput.addEventListener('keypress', (e) => { 4697 if (e.key === 'Enter') { 4698 const v = (elements.searchInput.value || '').trim(); 4699 if (v.length < 2) return; 4700 // 1ä»¶ã ãããã â ãã®åå㸠/ 2ä»¶ä»¥ä¸ â æ¤ç´¢ä¸è¦§ãã¼ã¸ã¸ 4701 if (v === _dropdownQuery && _dropdownResults.length === 1) { 4702 hideDropdown(); currentFrom = 'search'; _updateBackBtn('search'); showProduct(_dropdownResults[0].jan); 4703 } else { 4704 window.location.href = '/search?q=' + encodeURIComponent(v); 4705 } 4706 } 4707 }); 4708 elements.searchInput.addEventListener('focus', () => { if (elements.searchInput.value.trim().length >= 2) debouncedSearch(); }); 4709 document.addEventListener('click', (e) => { if (!elements.searchInput.contains(e.target) && !elements.searchDropdown.contains(e.target)) hideDropdown(); }); 4710 4711 elements.backBtn.addEventListener('click', () => goBack(false)); 4712 // ã³ãã¼ã¯å ±éã® jan-copy.jsï¼.kx-jan-copy ã¸ã®å§è²ï¼ãåãæã¤ã 4713 // ããã§äºéã«ç»é²ãã㨠1 ã¿ããã§ 2 åã³ãã¼ï¼ãã¼ã¹ãã 2 ã¤åºãã 4714 4715 // Inline search on product page 4716 (function(){ 4717 const inp = document.getElementById('inlineSearchInput'); 4718 const dd = document.getElementById('inlineSearchDropdown'); 4719 if (!inp || !dd) return; 4720 let timer; 4721 var _inlineResults = []; 4722 var _inlineQuery = ''; 4723 var _inlineHasMore = true; 4724 var _inlineLoading = false; 4725 var _INLINE_PAGE = 30; 4726 function _renderInlineBody(){ 4727 var html = _inlineResults.map(function(r){ 4728 var img = r.image_url 4729 ? '<img src="' + escapeHtml(r.image_url) + '" loading="lazy" alt="" style="width:36px;height:36px;object-fit:contain;border-radius:4px;background:#fff;flex-shrink:0;">' 4730 : '<div style="width:36px;height:36px;border-radius:4px;background:var(--border);flex-shrink:0;"></div>'; 4731 return '<a href="/dp/' + r.jan + '" style="display:flex;align-items:center;gap:10px;">' + img + '<div style="flex:1;min-width:0;"><div style="font-weight:500;">
4731' + escapeHtml(r.name).substring(0,60) + '</div><div style="font-size:0.75rem;color:var(--text-muted);">JAN: ' + r.jan + '</div></div></a>'; 4732 }).join(''); 4733 if (_inlineHasMore) { 4734 var label = _inlineLoading ? 'èªã¿è¾¼ã¿ä¸...' : 'ããã«èªã¿è¾¼ã (+' + _INLINE_PAGE + ')'; 4735 html += '<div id="inlineLoadMore" style="padding:10px;text-align:center;cursor:pointer;color:var(--primary);font-weight:600;font-size:0.85rem;border-top:1px solid var(--border);">' + label + '</div>'; 4736 } 4737 html += _searchAllRow(_inlineQuery, _inlineResults.length !== 1); 4738 dd.innerHTML = html; 4739 dd.classList.add('active'); 4740 var moreBtn = document.getElementById('inlineLoadMore'); 4741 if (moreBtn) { 4742 moreBtn.addEventListener('click', async function(e){ 4743 e.stopPropagation(); 4744 if (_inlineLoading || !_inlineHasMore) return; 4745 _inlineLoading = true; 4746 _renderInlineBody(); 4747 try { 4748 const token = (typeof CONFIG !== 'undefined' && CONFIG.API_TOKEN) ? CONFIG.API_TOKEN : ''; 4749 const offset = _inlineResults.length; 4750 const res = await fetch('/api/search?q=' + encodeURIComponent(_inlineQuery) + '&limit=' + _INLINE_PAGE + '&offset=' + offset, {headers: {'X-API-Token': token}}); 4751 const data = await res.json(); 4752 var more = (data && data.results) || []; 4753 _inlineResults = _inlineResults.concat(more); 4754 _inlineHasMore = more.length >= _INLINE_PAGE; 4755 } catch(err) { _inlineHasMore = false; } 4756 _inlineLoading = false; 4757 _renderInlineBody(); 4758 }); 4759 } 4760 } 4761 inp.addEventListener('input', function(){ 4762 clearTimeout(timer); 4763 const q = inp.value.trim(); 4764 if (q.length < 2) { dd.classList.remove('active'); return; } 4765 timer = setTimeout(async function(){ 4766 try { 4767 const token = (typeof CONFIG !== 'undefined' && CONFIG.API_TOKEN) ? CONFIG.API_TOKEN : ''; 4768 _inlineQuery = q; 4769 const res = await fetch('/api/search?q=' + encodeURIComponent(q) + '&limit=' + _INLINE_PAGE + '&offset=0', {headers: {'X-API-Token': token}}); 4770 const data = await res.json(); 4771 if (!data.results || !data.results.length) { dd.innerHTML = '<div style="padding:12px 16px;text-align:center;color:var(--text-muted);font-size:0.88rem;"><div style="margin-bottom:6px;">ã' + escapeHtml(q) + 'ãã®æ¤ç´¢çµæã¯ããã¾ãã</div><div style="font-size:0.8rem;">åååã®ä¸é¨ã»JANã»ASINã§æ¤ç´¢ãã¦ã¿ã¦ãã ãã</div></div>' + _searchAllRow(q); dd.classList.add('active'); return; } 4772 _inlineResults = data.results; 4773 _inlineHasMore = _inlineResults.length >= _INLINE_PAGE; 4774 _inlineLoading = false; 4775 _renderInlineBody(); 4776 } catch(e) { dd.classList.remove('active'); } 4777 }, 300); 4778 }); 4779 inp.addEventListener('keypress', function(e){ if (e.key === 'Enter') { e.preventDefault(); var v=(inp.value||'').trim(); if(v.length<2) return; if(v===_inlineQuery && _inlineResults.length===1){ showProduct(_inlineResults[0].jan); } else { window.location.href='/search?q='+encodeURIComponent(v); } }}); 4780 document.addEventListener('click', function(e){ if (!inp.contains(e.target) && !dd.contains(e.target)) dd.classList.remove('active'); }); 4781 })(); 4782 4783 document.getElementById('carouselPrev').addEventListener('click', carouselPrevPage); 4784 document.getElementById('carouselNext').addEventListener('click', carouselNextPage); 4785 document.getElementById('profitableToggle').addEventListener('click', toggleProfitableContent); 4786 document.getElementById('clearHistoryBtn').addEventListener('click', clearHistory); 4787 4788 // Category tab + collapse 4789 const _tabSearchBtn = document.getElementById('tabSearchCount'); 4790 const _tabCatBtn = document.getElementById('tabCategory'); 4791 const _popCollapseBtn = document.getElementById('popularCollapseBtn'); 4792 const _hisCollapseBtn = document.getElementById('historyCollapseBtn'); 4793 if (_tabSearchBtn) _tabSearchBtn.addEventListener('click', () => switchPopularTab('search_count')); 4794 if (_tabCatBtn) _tabCatBtn.addEventListener('click', () => switchPopularTab('category')); 4795 if (_popCollapseBtn) _popCollapseBtn.addEventListener('click', () => toggleCollapse('popularBody', 'popularCollapseBtn', 'kaitorix_popular_collapsed')); 4796 if (_hisCollapseBtn) _hisCollapseBtn.addEventListener('click', () => toggleCollapse('historyBody', 'historyCollapseBtn', 'kaitorix_history_collapsed')); 4797
4798 const qtyInput = document.getElementById('qtyInput'); 4799 document.getElementById('qtyMinus').addEventListener('click', () => { qtyInput.value = Math.max(1, (parseInt(qtyInput.value)||1) - 1); }); 4800 document.getElementById('qtyPlus').addEventListener('click', () => { qtyInput.value = Math.min(999, (parseInt(qtyInput.value)||1) + 1); }); 4801 document.getElementById('matomeAddBtn').addEventListener('click', () => { 4802 if(!currentProductData) return; 4803 const res = addToMatome(currentProductData.jan, currentProductData.name, parseInt(qtyInput.value)||1, currentProductData.image_url); 4804 const fb = document.getElementById('matomeAddFeedback'); 4805 fb.textContent = res.message; fb.style.color = res.success ? 'var(--success)' : 'var(--danger)'; 4806 if(res.success) { qtyInput.value = 1; updateMatomeUI(); } 4807 setTimeout(() => fb.textContent = '', 3000); 4808 }); 4809 4810 // PC: profit calculator 4811 const profitInput = document.getElementById('profitCalcInput'); 4812 if (profitInput) { 4813 profitInput.addEventListener('input', () => { 4814 const cost = parseInt(profitInput.value) || 0; 4815 const resultEl = document.getElementById('profitCalcResult'); 4816 if (!currentProductData || !currentProductData.prices || currentProductData.prices.length === 0 || cost <= 0) { 4817 resultEl.textContent = 'å©ç Â¥---'; 4818 resultEl.className = 'profit-calc-result zero'; 4819 return; 4820 } 4821 // ð´ ç´ ã®æé«å¤ã§è¨ç®ãããæ¡ä»¶ä»ãã®é¡ï¼30ç¹ä»¥ä¸ã»æªéå° çï¼ã§ 4822 // å©çãåºãã¨ãæ¡ä»¶ãæºãããªã人ã«å±ããªãæ°åãè¦ããäºã«ãªãã 4823 const _pr = currentProductData.prices || []; 4824 const maxPrice = rawMaxPrice(_pr) || deobfuscatePrice(_pr[0].price); 4825 const profit = maxPrice - cost; 4826 if (profit > 0) { 4827 resultEl.textContent = `å©ç +Â¥${profit.toLocaleString()}`; 4828 resultEl.className = 'profit-calc-result positive'; 4829 } else if (profit < 0) { 4830 resultEl.textContent = `æå¤± -Â¥${Math.abs(profit).toLocaleString()}`; 4831 resultEl.className = 'profit-calc-result negative'; 4832 } else { 4833 resultEl.textContent = 'å©ç ±¥0'; 4834 resultEl.className = 'profit-calc-result zero'; 4835 } 4836 }); 4837 } 4838 4839 // PC: matome qty buttons + add button 4840 const qtyInputPc = document.querySelector('.qty-input-pc'); 4841 document.querySelectorAll('.qty-btn-pc').forEach(btn => { 4842 btn.addEventListener('click', () => { 4843 if (!qtyInputPc) return; 4844 if (btn.dataset.action === 'minus') qtyInputPc.value = Math.max(1, (parseInt(qtyInputPc.value)||1) - 1); 4845 else qtyInputPc.value = Math.min(999, (parseInt(qtyInputPc.value)||1) + 1); 4846 }); 4847 }); 4848 const matomeAddBtnPc = document.getElementById('matomeAddBtnPc'); 4849 if (matomeAddBtnPc) { 4850 matomeAddBtnPc.addEventListener('click', () => { 4851 if(!currentProductData) return; 4852 const res = addToMatome(currentProductData.jan, currentProductData.name, parseInt(qtyInputPc.value)||1, currentProductData.image_url); 4853 if(res.success) { qtyInputPc.value = 1; updateMatomeUI(); } 4854 }); 4855 } 4856 4857 document.getElementById('matomeExpandBtn').addEventListener('click', expandMatomeBar); 4858 document.getElementById('matomeCollapseBtn').addEventListener('click', collapseMatomeBar); 4859 document.getElementById('matomeCompareBtn').addEventListener('click', startMatomeComparison); 4860 document.getElementById('matomeCompareBtnExpanded').addEventListener('click', startMatomeComparison); 4861 document.getElementById('matomeClearBtn').addEventListener('click', clearMatome); 4862 document.getElementById('matomeBackBtn').addEventListener('click', goBackFromMatomeComparison); 4863 4864 // æéåæ¿: ã³ã³ãã¯ãã»ã¬ã¯ãï¼ãã©ã·ã§ä»»æç¯å²ã鏿å¯ï¼ 4865 const chartRangeSel = document.getElementById('chartRangeSel'); 4866 if (chartRangeSel) { 4867 chartRangeSel.addEventListener('change', () => { 4868 const d = parseInt(chartRangeSel.value); 4869 if (d > 0 && currentHistoryData) {
4870 _saveChartPeriod(d); // ã¦ã¼ã¶ã¼é¸æãè¨æ¶ï¼æ¬¡åã»ä»ååã§ãæ¢å®ã«ï¼ 4871 renderChart(currentHistoryData, d); 4872 } 4873 }); 4874 } 4875 4876 document.getElementById('favoriteStarBtn').addEventListener('click', toggleFavorite); 4877 4878 // Share button 4879 const shareBtn = document.getElementById('shareBtn'); 4880 const shareMenu = document.getElementById('shareMenu'); 4881 shareBtn.addEventListener('click', (e) => { 4882 e.stopPropagation(); 4883 shareMenu.classList.toggle('active'); 4884 }); 4885 document.addEventListener('click', () => shareMenu.classList.remove('active')); 4886 shareMenu.addEventListener('click', (e) => e.stopPropagation()); 4887 4888 function getShareText() { 4889 if (!currentProductData) return { text: '', url: '' }; 4890 const p = currentProductData; 4891 const url = `https://kaitorix.app/dp/${p.jan}`; 4892 const prices = p.prices || []; 4893 let lines = [`ð¦ ${p.name}`, '']; 4894 if (prices.length > 0) { 4895 lines.push('ãè²·åä¾¡æ ¼ä¸è¦§ã'); 4896 prices.forEach((s, i) => { 4897 const prefix = i === 0 ? 'ð¥' : i === 1 ? 'ð¥' : i === 2 ? 'ð¥' : 'ã'; 4898 // ð´ ãã£ã³ãã¼ã³ä¾¡ã¯æ¡ä»¶ã¨ã»ããã§ããå¤ã¸åºããªããå ±ææã¯ 4899 // X/LINE/ã¯ãªãããã¼ãã¸ãã®ã¾ã¾è²¼ããããããã®æ³¨è¨ã¯ä»ã㦠4900 // è¡ããªãã®ã§ãããã§æ³¨è¨ãæåã¨ãã¦åè¡ãããã 4901 const _c = s.campaign; 4902 const _cond = (_c && _c.text) ? `ï¼${_c.text}ï¼æ¡ä»¶ããï¼` : ''; 4903 lines.push(`${prefix} ${s.store}ï¼Â¥${deobfuscatePrice(s.price).toLocaleString()}${_cond}`); 4904 }); 4905 lines.push(''); 4906 } 4907 lines.push(`â¶ è²·åX ã§ææ°ä¾¡æ ¼ããã§ãã¯`); 4908 return { text: lines.join('\n'), url }; 4909 } 4910 4911 document.getElementById('shareX').addEventListener('click', () => { 4912 const { text, url } = getShareText(); 4913 if (!text) return; 4914 window.open(`https://x.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`, '_blank'); 4915 shareMenu.classList.remove('active'); 4916 }); 4917 document.getElementById('shareLINE').addEventListener('click', () => { 4918 const { text, url } = getShareText(); 4919 if (!text) return; 4920 window.open(`https://line.me/R/share?text=${encodeURIComponent(text + '\n' + url)}`, '_blank'); 4921 shareMenu.classList.remove('active'); 4922 }); 4923 document.getElementById('shareCopy').addEventListener('click', () => { 4924 const { text, url } = getShareText(); 4925 if (!text) return; 4926 copyToClipboard(text + '\n' + url, 'ãªã³ã¯ãã³ãã¼ãã¾ãã'); 4927 shareMenu.classList.remove('active'); 4928 }); 4929 } 4930 4931 // ãã¼ã (æ¤ç´¢ããã)å°ç¨ã»ã¯ã·ã§ã³ã®é å»¶ãã¼ãã 4932 // ååãã¼ã¸(/dp/{jan} ç)ã§ååãã¼ããã㨠init ã¯ããããã¹ãããããããã 4933 // å¾ã§ goBack ã§ãããã«æ»ã£ãæã«ã人æ°/å©çåå/æè¿è¦ããã空ã®ã¾ã¾ã«ãªã 4934 // ï¼å±¥æ´ã¯ localStorage åæãã¼ããªã®ã§å¸¸ã«åºãï¼ãåªçã«ãã¦ä¸¡çµè·¯ããå¼ã¶ã 4935 let _homeSectionsLoaded = false; 4936 function ensureHomeSectionsLoaded() { 4937 if (_homeSectionsLoaded) return; 4938 _homeSectionsLoaded = true; 4939 loadProfitableProducts(); 4940 loadDealTicker(); 4941 loadPopularSearches(); 4942 loadRecentViewed(); 4943 } 4944 4945 function init() { 4946 bindEvents(); 4947 const _isProductPage = /^\/(?:dp|product)\/\d{7,14}$/.test(window.location.pathname) 4948 || new URLSearchParams(window.location.search).has('jan'); 4949 if (!_isProductPage) { 4950 ensureHomeSectionsLoaded(); 4951 } 4952 loadProfitPreview(); 4953 loadHistory(); 4954 restoreCollapsedState(); 4955 updateMatomeUI(); 4956 4957 const pathMatch = window.location.pathname.match(/^\/(?:dp|product)\/(\d{7,14})$/);
4958 const urlParams = new URLSearchParams(window.location.search); 4959 const urlJan = urlParams.get('jan'); 4960 const urlQuery = urlParams.get('q'); 4961 const fromVal = urlParams.get('from'); 4962 if (fromVal && _backConfig[fromVal]) { 4963 currentFrom = fromVal; 4964 _updateBackBtn(fromVal); 4965 // from_url: æ»ãå ã®å ·ä½ç URLï¼listing.html ããä»ä¸ï¼ 4966 const fromUrlVal = urlParams.get('from_url'); 4967 if (fromUrlVal) { 4968 try { 4969 // from_url 㯠/ranking/price-up çã®çµ¶å¯¾ãã¹ 4970 const decoded = decodeURIComponent(fromUrlVal); 4971 if (decoded.startsWith('/')) currentFromUrl = decoded; 4972 } catch(e) {} 4973 } 4974 // from/from_url 以å¤ã® query params ãä¿åï¼source çï¼ 4975 const backParams = new URLSearchParams(urlParams); 4976 backParams.delete('from'); 4977 backParams.delete('from_url'); 4978 currentFromQuery = backParams.toString(); 4979 } 4980 4981 const urlMatome = decodeMatomFromUrl(); 4982 if (urlMatome) { 4983 // URL ããã¾ã¨ã復å ãã¦èªåæ¯è¼ 4984 saveMatomeToStorage({ items: [], updatedAt: Date.now() }); 4985 (async function() { 4986 // å ±æ URL ã®ã¾ã¨ãã¯ä»¶æ°ãå¤ãï¼=ç¸æãåãç½ ãè¸ãï¼ã®ã§ä¸æ¬ã§åã 4987 const got = await fetchProductsBatch(urlMatome.map(it => it.jan)); 4988 urlMatome.forEach(function(item, i) { 4989 const d = got[i]; 4990 addToMatome(item.jan, (d && d.name) || item.jan, item.quantity, 4991 (d && d.image_url) || ''); 4992 }); 4993 updateMatomeUI(); 4994 if (new URLSearchParams(location.search).get('matome') === 'compare') { 4995 startMatomeComparison(); 4996 } 4997 })(); 4998 } else if (urlParams.get('matome') === 'compare') { 4999 setTimeout(() => startMatomeComparison(), 300); 5000 } else if (pathMatch) { _isInitialLoad = true; showProduct(pathMatch[1], true); } 5001 else if (urlJan) { _isInitialLoad = true; showProduct(urlJan, true); } 5002 else if (urlQuery) { 5003 elements.searchInput.value = urlQuery; 5004 syncEnterHint(); // value ãããã°ã©ã ããå ¥ããå ´å㯠input ã¤ãã³ããé£ã°ãªã 5005 // 1ä»¶ã®ã¿ããããªãç´æ¥ååãã¼ã¸ã¸ 5006 apiCall('/api/search?q=' + encodeURIComponent(urlQuery) + '&limit=2').then(function(data) { 5007 if (data.results && data.results.length === 1) { 5008 _isInitialLoad = true; 5009 showProduct(data.results[0].jan, true); 5010 } else { 5011 searchDropdown(urlQuery); 5012 } 5013 }).catch(function() { searchDropdown(urlQuery); }); 5014 } 5015 else elements.searchInput.focus(); 5016 5017 // GA4: å¤é¨ã¢ãã£ãªã¨ã¤ããªã³ã¯ã¯ãªãã¯è¿½è·¡ 5018 document.addEventListener('click', function(e) { 5019 var link = e.target.closest('a[href*="amazon.co.jp"], a[href*="rakuten.co.jp"], a[href*="shopping.yahoo.co.jp"], a[href*="mottoku.app"]'); 5020 if (link && window.gtag) { 5021 gtag('event', 'click', { 5022 event_category: 'affiliate', 5023 event_label: link.hostname, 5024 transport_type: 'beacon' 5025 }); 5026 } 5027 }); 5028 5029 trackPageView(); 5030 window.addEventListener('popstate', () => { 5031 const m = window.location.pathname.match(/^\/(?:dp|product)\/(\d{7,14})$/); 5032 if (m) { 5033 // URL ã« from= ãããã°å¾©å 5034 const pp = new URLSearchParams(window.location.search); 5035 const pf = pp.get('from'); 5036 if (pf && _backConfig[pf]) { 5037 currentFrom = pf; _updateBackBtn(pf); 5038 const pfUrl = pp.get('from_url'); 5039 if (pfUrl) { 5040 try { 5041 const decoded = decodeURIComponent(pfUrl); 5042 if (decoded.startsWith('/')) currentFromUrl = decoded; 5043 } catch(e) {} 5044 } else { currentFromUrl = null; } 5045 const bp = new URLSearchParams(pp); bp.delete('from'); bp.delete('from_url'); 5046 currentFromQuery = bp.toString(); 5047 } 5048 showProduct(m[1]); 5049 } else { 5050 currentFrom = null; 5051 _updateBackBtn(null); 5052 goBack(true); 5053 } 5054 }); 5055 } 5056 5057 // Amazon/Deal ã¯ãªãã¯çµ±è¨ 5058 document.addEventListener('click', function(e) { 5059 var a = e.target.closest('a[href*="amazon.co.jp"], a[href*="kaitorix.app/deal/"]'); 5060 if (!a) return; 5061 var href = a.href || ''; 5062 var asin = ''; 5063 var m = href.match(/\/dp\/([A-Z0-9]{10})/i) || href.match(/\/deal\/([A-Z0-9]{10})/i); 5064 if (m) asin = m[1].toUpperCase(); 5065 navigator.sendBeacon('/api/deal-click', JSON.stringify({r: document.referrer || location.origin, d: document.referrer || '', a: asin, s: 'web'})); 5066 }); 5067 5068 document.addEventListener('DOMContentLoaded', init); 5069 5070 // Hero particles 5071 document.addEventListener('DOMContentLoaded', function() { 5072 const c = document.getElementById('heroParticlesMain'); 5073 if (!c) return; 5074 for (let i = 0; i < 15; i++) { 5075 const s = document.createElement('span'); 5076 const sz = Math.random() * 5 + 2; 5077 s.style.width = sz + 'px'; 5078 s.style.height = sz + 'px'; 5079 s.style.left = Math.random() * 100 + '%'; 5080 s.style.bottom = (Math.random() * 60 + 10) + '%'; 5081 s.style.animationDuration = (Math.random() * 4 + 3) + 's'; 5082 s.style.animationDelay = (Math.random() * 5) + 's'; 5083 c.appendChild(s); 5084 } 5085 }); 5086 })(); 5087 5088 5089 // Service Worker ç»é²ï¼PWA ã¤ã³ã¹ãã¼ã«ç¨ï¼ 5090 if ('serviceWorker' in navigator) { 5091 navigator.serviceWorker.register('/sw.js'); 5092 } 5093 5094 // Barcode Scanner â native BarcodeDetector (Android) + zxing-wasm engine (iOS/ãã®ä»)
5095 // iOS Safari 㯠BarcodeDetector é対å¿ã®ãããèªãã¹ãã® zxing-wasm(WebAssembly)ã§å ¨ç«¯æ«ãé«ç²¾åº¦ã«ãã¼ã 5096 (function(){ 5097 var scanBtn = document.getElementById('barcodeScanBtn'); 5098 var overlay = document.getElementById('scannerOverlay'); 5099 var video = document.getElementById('scannerVideo'); 5100 var closeBtn = document.getElementById('scannerClose'); 5101 var canvas = document.getElementById('scannerCanvas'); 5102 if (!scanBtn || !video || !canvas) return; 5103 // 追å ããªã¬ã¼ï¼/dp ã¤ã³ã©ã¤ã³æ¤ç´¢è¡ï¼ã¨é£ç¶ã¹ãã£ã³ UIï¼ç¡ããã°å¾æ¥åä½ã®ã¾ã¾ï¼ 5104 var inlineScanBtn = document.getElementById('inlineScanBtn'); 5105 var modeToggle = document.getElementById('scanModeToggle'); 5106 var scoutList = document.getElementById('scoutList'); 5107 var scoutCount = document.getElementById('scoutCount'); 5108 var scoutClear = document.getElementById('scoutClear'); 5109 var sheet = document.getElementById('scoutSheet'); 5110 var sheetFrame = document.getElementById('scoutSheetFrame'); 5111 var sheetBack = document.getElementById('scoutSheetBack'); 5112 var sheetExit = document.getElementById('scoutSheetExit'); 5113 5114 var ZXING_BASE = '/js/zxing/'; 5115 var ZXING_WASM_V = '310'; // wasm ãã£ãã·ã¥ç¨ãã¼ã¸ã§ã³ï¼zxing-wasm 3.1.0ï¼ 5116 var ZXING_FORMATS = ['EAN-13', 'EAN-8', 'UPC-A', 'UPC-E']; // JAN/UPC ã®ã¿ 5117 var ZXING_OPTS = { formats: ZXING_FORMATS, tryHarder: true, maxNumberOfSymbols: 1 }; 5118 5119 var stream = null; 5120 var scanning = false; 5121 var rafId = null; 5122 var detector = null; // native BarcodeDetector instanceï¼å©ç¨å¯è½æï¼ 5123 var zxingRead = null; // zxing-wasm ã® readBarcodesï¼é å»¶ãã¼ãï¼ 5124 var useNative = false; 5125 var confirmBuffer = []; 5126 var CONFIRM_COUNT = 2; // é£ç¶åä¸ã³ã¼ãã§ç¢ºå®ï¼checksum æ¤è¨¼æ¸ãªã®ã§ 2 ã§ååï¼ 5127 5128 // ---- ã«ã¡ã©ã¹ããªã¼ã åå©ç¨: éãã¦ãå³ stop ããç¶äºå ã®åãªã¼ãã³ã§å権éè¦æ±ãåé¿ ---- 5129 var releaseTimer = null; 5130 var STREAM_GRACE_MS = 90000; // ç¶äºï¼ãã®éã«ã¡ã©ã¯ç¹ããã¾ã¾ããã©ã¤ãã·ã¼ã¨ã®ãã©ã³ã¹ï¼ 5131 5132 // ---- é£ç¶ã¹ãã£ã³ï¼ä»å ¥ãï¼ã¢ã¼ãç¶æ ---- 5133 var mode = 'single'; 5134 try { if (localStorage.getItem('kaitorix_scan_mode') === 'scout') mode = 'scout'; } catch (e) {} 5135 var scoutHistory = loadScout(); // [{jan, name?, img?, p?, ts}] æ°ããé ã»æå¤§100ä»¶ 5136 // 復帰ç´å¾ã®å³åã¹ãã£ã³æ´çºå¯¾ç: 5137 // scanCooldownUntil = ã·ã¼ãããæ»ã£ãå¾ãã®æå»ã¾ã§å ¨æ¤åºãç¡è¦ï¼æã®ç§»åç¶äºï¼ 5138 // suppress = {code: æçµç®æts} ç´åã«ã·ã¼ã表示ããã³ã¼ãã¯ç»é¢å ã«æ ãç¶ããéãåçºç«ããªã 5139 var scanCooldownUntil = 0; 5140 var suppress = {}; 5141 var lastScanCode = ''; 5142 var nameTimer = null; 5143 var sheetJan = ''; 5144 var lastSnapTs = 0; 5145 // ã·ã¼ãééç´å¾ã®ã´ã¼ã¹ã click 対çï¼touchend å¾ã®é å»¶ click ãã·ã¼ãæ¶å¤±ã§ 5146 // ä¸å±¤ã®å±¥æ´ãªã¹ãçã«çå¼¾ â æå³ããåãªã¼ãã³ããã®ã 600ms ææ¢ï¼ 5147 var sheetMovedAt = 0; 5148 function sheetJustMoved() { return Date.now() - sheetMovedAt < 600; } 5149 5150 // EAN-8 / UPC-A / EAN-13(=JAN) ã®æ¡æ°ã®ã¿åçï¼èª¤æ¤åºã¬ã¼ãããã§ãã¯ãã¸ãã㯠reader å´ã§æ¤è¨¼æ¸ï¼ 5151 function isValidCode(code) { 5152 if (window.KXBarcode && window.KXBarcode.isValidCode) return window.KXBarcode.isValidCode(code); 5153 return /^(\d{8}|\d{12,13})$/.test(code); 5154 } 5155 5156 function onDetected(raw) { 5157 var code = (raw || '').trim(); 5158 if (!isValidCode(code)) { confirmBuffer = []; return; } 5159 confirmBuffer.push(code); 5160 if (confirmBuffer.length > CONFIRM_COUNT) confirmBuffer.shift(); 5161 if (confirmBuffer.length === CONFIRM_COUNT && 5162 confirmBuffer.every(function(c){ return c === code; })) { 5163 if (mode === 'scout') { 5164 var now = Date.now(); 5165 // ã·ã¼ãããæ»ã£ãç´å¾ã¯å ¨æ¤åºãç¡è¦ï¼ã«ã¡ã©ãã¾ã æ£/ãã¼ã³ã¼ãé¢ãåããã¾ã¾ 5166 // â ä½ãããå³ããããã¦åæã«ã·ã¼ããåãªã¼ãã³ããæ´çºã®é²æ¢ï¼ 5167 if (now < scanCooldownUntil) { 5168 if (suppress[code] !== undefined) suppress[code] = now; // ç®æç¶ç¶ãè¨é² 5169 confirmBuffer = []; 5170 return; 5171 } 5172 // ç´åã«ã·ã¼ã表示ããã³ã¼ã: ç»é¢å ã«æ ãç¶ããéãçºç«ããªãï¼ç®æãã¨ã« ts æ´æ°ï¼ã 5173 // 2.5 ç§ç»é¢å¤ã«åºã¦ããå度æ ã£ãå ´åã®ã
5173¿ãæå³çãªåã¹ãã£ã³ãã¨ãã¦è¨±å¯ã 5174 if (suppress[code] !== undefined) { 5175 if (now - suppress[code] < 2500) { suppress[code] = now; confirmBuffer = []; return; } 5176 delete suppress[code]; 5177 } 5178 if (navigator.vibrate) { try { navigator.vibrate(80); } catch (e) {} } 5179 lastScanCode = code; 5180 pauseLoop(); // ã«ã¡ã©ã¯çãããã¾ã¾æ¤åºã«ã¼ãã ãæ¢ãã 5181 scoutAdd(code); 5182 openSheet(code); 5183 return; 5184 } 5185 if (navigator.vibrate) { try { navigator.vibrate(80); } catch (e) {} } // å½ä¸æã®è§¦è¦ãã£ã¼ããã㯠5186 closeScanner(); 5187 window.location.href = '/dp/' + code; 5188 } 5189 } 5190 5191 // æ¤åºã«ã¼ãã®ã¿åæ¢ï¼ã¹ããªã¼ã ã¯ç¶æï¼ 5192 function pauseLoop() { 5193 scanning = false; 5194 if (rafId) { cancelAnimationFrame(rafId); rafId = null; } 5195 confirmBuffer = []; 5196 } 5197 5198 function streamLive() { 5199 try { 5200 return !!stream && stream.getVideoTracks().some(function(t){ return t.readyState === 'live'; }); 5201 } catch (e) { return false; } 5202 } 5203 5204 // ã«ã¡ã©ãå®éã«è§£æ¾ï¼ç¶äºã¿ã¤ãã¼æºäºã»ãã¼ã¸é¢è±æã®ã¿ï¼ 5205 function releaseStream() { 5206 if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null; } 5207 if (stream) { try { stream.getTracks().forEach(function(t){ t.stop(); }); } catch (e) {} stream = null; } 5208 video.srcObject = null; 5209 } 5210 5211 // ãªã¼ãã¼ã¬ã¤ãéããï¼ã¹ããªã¼ã 㯠STREAM_GRACE_MS ã ãæ¸©å â åãªã¼ãã³å³æï¼ 5212 function closeScanner() { 5213 pauseLoop(); 5214 overlay.classList.remove('active'); 5215 document.body.classList.remove('kx-scan-lock'); 5216 closeSheet(false); 5217 if (releaseTimer) clearTimeout(releaseTimer); 5218 releaseTimer = setTimeout(releaseStream, STREAM_GRACE_MS); 5219 } 5220 5221 // ==================== é£ç¶ã¹ãã£ã³ï¼ä»å ¥ãï¼ã¢ã¼ã ==================== 5222 function loadScout() { 5223 try { 5224 var a = JSON.parse(localStorage.getItem('kaitorix_scan_history') || '[]'); 5225 return Array.isArray(a) ? a : []; 5226 } catch (e) { return []; } 5227 } 5228 function saveScout() { 5229 try { localStorage.setItem('kaitorix_scan_history', JSON.stringify(scoutHistory.slice(0, 100))); } catch (e) {} 5230 } 5231 function api(ep) { 5232 return (window.KX_CORE && window.KX_CORE.apiCall) 5233 ? window.KX_CORE.apiCall(ep) 5234 : Promise.reject(new Error('KX_CORE unavailable')); 5235 } 5236 5237 function setMode(m) { 5238 mode = (m === 'scout') ? 'scout' : 'single'; 5239 try { localStorage.setItem('kaitorix_scan_mode', mode); } catch (e) {} 5240 applyMode(); 5241 } 5242 function applyMode() { 5243 overlay.classList.toggle('scout', mode === 'scout'); 5244 if (modeToggle) { 5245 modeToggle.querySelectorAll('.scan-mode-btn').forEach(function(b){ 5246 var on = b.getAttribute('data-mode') === mode; 5247 b.classList.toggle('is-active', on); 5248 b.setAttribute('aria-selected', on ? 'true' : 'false'); 5249 }); 5250 } 5251 if (mode === 'scout' && overlay.classList.contains('active')) { 5252 renderScout(); 5253 // å±¥æ´ã®ä¾¡æ ¼ã䏿¬ãªãã¬ãã·ã¥ï¼30ç§ã¹ãããã«ãå é 24ä»¶ï¼ 5254 var now = Date.now(); 5255 if (scoutHistory.length && now - lastSnapTs > 30000) { 5256 lastSnapTs = now; 5257 fetchSnap(scoutHistory.map(function(e){ return e.jan; })); 5258 } 5259 } 5260 } 5261 5262 function scoutAdd(code) { 5263 var entry = null; 5264 for (var i = 0; i < scoutHistory.length; i++) { 5265 if (scoutHistory[i].jan === code) { entry = scoutHistory.splice(i, 1)[0]; break; } 5266 } 5267 if (!entry) entry = { jan: code }; 5268 entry.ts = Date.now(); 5269 scoutHistory.unshift(entry); 5270 if (scoutHistory.length > 100) scoutHistory.length = 100; 5271 saveScout(); 5272 renderScout(); 5273 fetchSnap([code]); 5274 } 5275 5276 // /api/price-snap ã§æé«è²·åä¾¡æ ¼ï¼ç»åãè£å®ï¼KX_CORE çµç±å¿ é : 裸 fetch ã¯æ¬çªã§ token å¼¾ãï¼ 5277 function fetchSnap(jans) { 5278 if (!jans || !jans.length) return; 5279 api('/api/price-snap?jans=' + jans.slice(0, 24).join(',')).then(function(d){ 5280 if (!d) return; 5281 var dirty = false;
5282 scoutHistory.forEach(function(e){ 5283 var s = d[e.jan]; 5284 if (s) { 5285 if (e.p !== s.p) { e.p = s.p || 0; dirty = true; } 5286 if (s.img && !e.img) { e.img = s.img; dirty = true; } 5287 } else if (jans.indexOf(e.jan) >= 0 && e.p === undefined) { 5288 e.p = 0; dirty = true; // DB æªç»é² â ãä¾¡æ ¼ãªãã表示ã«ç¢ºå® 5289 } 5290 }); 5291 if (dirty) { saveScout(); renderScout(); } 5292 }).catch(function(){}); 5293 } 5294 5295 function fmtScoutTime(ts) { 5296 var d = new Date(ts); 5297 return (d.getMonth() + 1) + '/' + d.getDate() + ' ' + 5298 ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2); 5299 } 5300 5301 function renderScout() { 5302 if (!scoutList) return; 5303 if (scoutCount) scoutCount.textContent = scoutHistory.length ? 'ï¼' + scoutHistory.length + 'ï¼' : ''; 5304 scoutList.textContent = ''; 5305 if (!scoutHistory.length) { 5306 var empty = document.createElement('div'); 5307 empty.className = 'scout-empty'; 5308 empty.textContent = 'ã¹ãã£ã³ããååãããã«è¿½å ããã¾ã'; 5309 scoutList.appendChild(empty); 5310 return; 5311 }
5312 scoutHistory.forEach(function(e){ 5313 var row = document.createElement('div'); 5314 row.className = 'scout-item'; 5315 row.setAttribute('data-jan', e.jan); 5316 row.setAttribute('role', 'button'); 5317 row.tabIndex = 0; 5318 if (e.img) { 5319 var im = document.createElement('img'); 5320 im.className = 'scout-item-img'; im.src = e.img; im.alt = ''; im.loading = 'lazy'; 5321 row.appendChild(im); 5322 } else { 5323 var ph = document.createElement('div'); 5324 ph.className = 'scout-item-imgph'; 5325 ph.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20"><path d="M3 7V5a2 2 0 0 1 2-2h2"></path><path d="M17 3h2a2 2 0 0 1 2 2v2"></path><path d="M21 17v2a2 2 0 0 1-2 2h-2"></path><path d="M7 21H5a2 2 0 0 1-2-2v-2"></path><line x1="7" y1="12" x2="17" y2="12"></line></svg>'; 5326 row.appendChild(ph); 5327 } 5328 var main = document.createElement('div'); 5329 main.className = 'scout-item-main'; 5330 var nm = document.createElement('div'); 5331 nm.className = 'scout-item-name'; 5332 nm.textContent = e.name || e.jan; 5333 main.appendChild(nm); 5334 var sub = document.createElement('div'); 5335 sub.className = 'scout-item-sub'; 5336 sub.textContent = (e.name ? 'JAN: ' + e.jan + ' ã» ' : '') + fmtScoutTime(e.ts); 5337 main.appendChild(sub); 5338 row.appendChild(main); 5339 var pr = document.createElement('div'); 5340 if (e.p > 0) { 5341 pr.className = 'scout-item-price'; 5342 pr.textContent = 'Â¥' + Number(e.p).toLocaleString(); 5343 } else { 5344 pr.className = 'scout-item-price none'; 5345 pr.textContent = (e.p === 0) ? 'ä¾¡æ ¼ãªã' : ''; 5346 } 5347 row.appendChild(pr); 5348 var del = document.createElement('button'); 5349 del.type = 'button'; 5350 del.className = 'scout-item-del'; 5351 del.setAttribute('data-del', e.jan); 5352 del.setAttribute('aria-label', 'å±¥æ´ããåé¤'); 5353 del.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'; 5354 row.appendChild(del); 5355 scoutList.appendChild(row); 5356 }); 5357 } 5358 5359 // ---- åå詳細ã·ã¼ãï¼åæº /dp ã iframe è¡¨ç¤ºï¼ ---- 5360 function openSheet(jan) { 5361 if (!sheet || !sheetFrame) { window.location.href = '/dp/' + jan; return; } 5362 sheetJan = jan; 5363 if (sheetFrame.getAttribute('data-jan') !== jan) { 5364 sheetFrame.setAttribute('data-jan', jan); 5365 sheetFrame.src = '/dp/' + jan; 5366 } else { 5367 stripSheetChrome(); // åä¸ååã®å表示: èªè¾¼æ¸ã¿ããã®ã¾ã¾ä½¿ã 5368 } 5369 sheet.classList.add('open'); 5370 sheet.setAttribute('aria-hidden', 'false'); 5371 sheetMovedAt = Date.now(); 5372 } 5373 function closeSheet(resume) { 5374 if (!sheet) return; 5375 var wasOpen = sheet.classList.contains('open'); 5376 sheet.classList.remove('open'); 5377 sheet.setAttribute('aria-hidden', 'true'); 5378 if (wasOpen) sheetMovedAt = Date.now(); 5379 if (nameTimer) { clearInterval(nameTimer); nameTimer = null; } 5380 if (resume && wasOpen && overlay.classList.contains('active') && mode === 'scout') resumeScan(); 5381 } 5382 function resumeScan() { 5383 if (scanning) return; 5384 // 復帰æ´çºã¬ã¼ã: 1.5s å ¨æ¤åºç¡è¦ + ç´åã®ã·ã¼ã対象ã³ã¼ããæå¶ãªã¹ãã«ç¨®ã¾ã 5385 var now = Date.now(); 5386 scanCooldownUntil = now + 1500; 5387 if (lastScanCode) suppress[lastScanCode] = now; 5388 if (sheetJan) suppress[sheetJan] = now; 5389 if (!streamLive()) { startScan(); return; } // ç¶äºåãçã§æ»ãã§ãããååå¾ 5390 scanning = true; 5391 confirmBuffer = []; 5392 (useNative ? nativeTick : zxingTick)(); 5393 } 5394 // iframe å ã®ãµã¤ã chromeï¼ãã¼ã«ãã¼/ã¿ããã¼/ããã¼çï¼ãé表示ã«ãã¦ååæ å ±ã«éä¸ããã 5395 function stripSheetChrome() { 5396 try { 5397 var d = sheetFrame.contentDocument; 5398 if (!d || d.getElementById('kxScoutStrip')) return; 5399 var st = d.createElement('style'); 5400 st.id = 'kxScoutStrip'; 5401 st.textContent = '.sticky-toolbar,#kxTabbar,.kx-tabbar,#dpMiniSticky,#kxDealTicker,#heroCarousel,.hero-dots,.discord-fab,.x-fab,#discordFab,footer,#inlineScanBtn,#barcodeScanBtn{display:none!important}'; 5402 (d.head || d.documentElement).appendChild(st); 5403 } catch (e) {} 5404 } 5405 // ååå㯠iframe ã® /dp ãæç»ãããã®ãæ¾ãï¼è¿½å API æ¶è²»ã¼ãã»ã¬ã¼ãå¶éã«åªããï¼ 5406 function pollSheetName() { 5407 if (nameTimer) clearInterval(nameTimer); 5408 var jan = sheetJan, tries = 0; 5409 var entry = null; 5410 for (var i = 0; i < scoutHistory.length; i++) { 5411 if (scoutHistory[i].jan === jan) { entry = scoutHistory[i]; break; } 5412 } 5413 if (!entry || entry.name) return; 5414 nameTimer = setInterval(function(){ 5415 tries++; 5416 try { 5417 var el = sheetFrame.contentDocument && sheetFrame.contentDocument.getElementById('productName'); 5418 var t = el ? (el.textContent || '').trim() : ''; 5419 if (t && t !== '-') { 5420 entry.name = t; 5421 saveScout(); renderScout(); 5422 clearInterval(nameTimer); nameTimer = null; 5423 return; 5424 } 5425 } catch (e) {} 5426 if (tries > 25) { clearInterval(nameTimer); nameTimer = null; } 5427 }, 400); 5428 } 5429 5430 // ROI 㯠1 種é¡ã§ã¯è¶³ããªãï¼2026-08-08ï¼ã 5431 // Amazon ã®ããã¤ã¹ç®±ï¼Kindle çï¼ã® UPC-A 㯠**ç¸¦ã«æ½°ãã¦ãã**ï¼å®æ¸¬ 1618x258pxï¼ 5432 // å¹ é«æ¯ 6.3:1ï¼æ¨æº UPC-A 㯠1.47:1ãã¤ã¾ãé«ã
5432ãæ¨æºã® 2 å²å¼·ãããªãï¼ã 5433 // å ¨ãã¼ã横åããèµ°æ»ç·ã極端ã«å°ãªããããå°ãå¾ããã ãã§è§£ããªããªãã 5434 // May ã®å®åçã§ãå¯ãå ·å à ROIã18 éããæ¬çªåä¸ã® zxing ã§åãã宿¸¬: 5435 // ä¸å¤®82%x60% ã ã = 6 éãä¸ 1 ããã / ä¸è¨ 3 種ãåã㨠3 ãããï¼3 åï¼ 5436 // â» è¿ã¥ãéãã¯å·¦å³ã® guard bar ãæ å¤ã«åºã¦**ã©ã® ROI ã§ãè§£ããªã**ï¼ 5437 // ãæ¨ªãã£ã±ãã«åãã»å¯ãéããªããã¨ããæ¡å ãè¦ãï¼UI å´ï¼ã 5438 // 3 種ã**åä¸ãã¬ã¼ã ã§å ¨é¨è©¦ãã®ã§ã¯ãªããã¬ã¼ã æ¯ã«åã**ï¼iOS ã®çºç±ã»é»æ± 対çã 5439 // 30fps ãªã 3 種ãç´ 100ms ã§ä¸å·¡ãã使ã¯å¤ãããªãï¼ã 5440 // å¤ã®æ£æ¬ã¯ /js/barcode-core.jsï¼search.html ã®ã¹ãã£ãã¨å ±æï¼ã 5441 // ð´ ãã©ã¼ã«ããã¯å¿ é : core ãèªããªãã¦ãã¹ãã£ããæ»ãªããªãã 5442 var ROI_VARIANTS = (window.KXBarcode && window.KXBarcode.ROI_VARIANTS) || [ 5443 { w: 0.82, h: 0.60 }, // 徿¥ï¼æ®éã®è·é¢ã§å¹ãï¼ 5444 { w: 1.00, h: 1.00 }, // å ¨ç»é¢ï¼å¼ãæ°å³ã§å¹ãã宿¸¬ã§ããã ãå½ãã忝ãããï¼ 5445 { w: 0.95, h: 0.72 } // åºãï¼ããå¯ãã§å¹ãï¼ 5446 ]; 5447 var roiIdx = 0; 5448 5449 // ã¬ã¤ãæ ä¸å¤®ã® ROI ãåãåºã㦠ImageData ãè¿ãï¼å¯¾è±¡ã大ããåããã¤ãºãæ¸ããï¼ 5450 function grabROI(variant) { 5451 var vw = video.videoWidth, vh = video.videoHeight; 5452 if (!vw || !vh) return null; 5453 var v = ROI_VARIANTS[variant || 0] || ROI_VARIANTS[0]; 5454 var cw = Math.max(1, Math.floor(vw * v.w)); 5455 var ch = Math.max(1, Math.floor(vh * v.h)); 5456 var sx = Math.floor((vw - cw) / 2), sy = Math.floor((vh - ch) / 2); 5457 canvas.width = cw; canvas.height = ch; 5458 var ctx = canvas.getContext('2d', { willReadFrequently: true }); 5459 ctx.drawImage(video, sx, sy, cw, ch, 0, 0, cw, ch); 5460 return ctx.getImageData(0, 0, cw, ch); 5461 } 5462 5463 // ---- ãã¤ãã£ã BarcodeDetector ã«ã¼ãï¼Chrome Androidï¼ã解決å¾ã«æ¬¡ãã¬ã¼ã ãäºç´ãéè¤å¼ã³åºããé²ã ---- 5464 function nativeTick() { 5465 if (!scanning) return; 5466 if (video.readyState < 2) { rafId = requestAnimationFrame(nativeTick); return; } 5467 detector.detect(video).then(function(codes){ 5468 if (codes && codes.length) onDetected(codes[0].rawValue); 5469 }).catch(function(){}).then(function(){ 5470 if (scanning) rafId = requestAnimationFrame(nativeTick); 5471 }); 5472 } 5473 5474 // ---- zxing-wasm ã«ã¼ãï¼iOS Safari ã»ãï¼ãROI ã® ImageData ã WASM ã¸ç´æ¥æ¸¡ã ---- 5475 function zxingTick() { 5476 if (!scanning) return; 5477 // ãã¬ã¼ã æ¯ã« ROI å¤ç¨®ãåãï¼æ½°ãããã¼ã³ã¼ã対çãä¸ã® ROI_VARIANTS åç §ï¼ 5478 var useIdx = roiIdx; 5479 roiIdx = (roiIdx + 1) % ROI_VARIANTS.length; 5480 var img = (video.readyState >= 2) ? grabROI(useIdx) : null; 5481 if (!img) { rafId = requestAnimationFrame(zxingTick); return; } 5482 zxingRead(img, ZXING_OPTS).then(function(results){ 5483 if (results && results.length) { 5484 for (var i = 0; i < results.length; i++) { 5485 if (results[i] && results[i].text) { onDetected(results[i].text); break; } 5486 } 5487 } 5488 }).catch(function(){}).then(function(){ 5489 if (scanning) rafId = requestAnimationFrame(zxingTick); 5490 }); 5491 } 5492 5493 // é£ç¶ãªã¼ããã©ã¼ã«ã¹ã使ãããªãé©ç¨ï¼è¿è·é¢ã®ãã¼ã³ã¼ãããã±ã«ãããªãï¼ 5494 function applyTrackEnhancements() { 5495 try { 5496 var track = stream.getVideoTracks()[0]; 5497 if (!track || !track.getCapabilities) return; 5498 var caps = track.getCapabilities(); 5499 if (caps.focusMode && caps.focusMode.indexOf('continuous') >= 0) { 5500 track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }).catch(function(){}); 5501 } 5502 } catch (e) { /* é対å¿ç«¯æ«ã¯ç¡è¦ */ } 5503 } 5504 5505 // getUserMedia 㯠3 段ãã©ã¼ã«ããã¯ï¼å 8 ç§ã¿ã¤ã ã¢ã¦ãï¼ç«¯æ«å·®ã§åºã¾ããªãããã«ï¼ 5506 function getCameraStream() { 5507 var withTimeout = function(constraints) { 5508 return Promise.race([ 5509 navigator.mediaDevices.getUserMedia(constraints), 5510 new Promise(function(_, rej){ setTimeout(function(){ rej(new Error('timeout')); }, 8000); }) 5511 ]); 5512 }; 5513 return withTimeout({ video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false }) 5514 .catch(function(){ return withTimeout({ video: { facingMode: 'environment' }, audio: false }); }) 5515 .catch(function(){ return withTimeout({ video: true, audio: false }); }); 5516 } 5517 5518 function startScan() { 5519 if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null; } 5520 var reuse = streamLive(); // ç¶äºå ã®åãªã¼ãã³ â getUserMedia ãåè¦æ±ããªã 5521 (reuse ? Promise.resolve(stream) : getCameraStream()).then(function(s) { 5522 stream = s; 5523 if (video.srcObject !== stream) video.srcObject = stream; 5524 if (video.play) { video.play().catch(function(){}); } 5525 overlay.classList.add('active'); 5526 document.body.classList.add('kx-scan-lock'); 5527 applyMode(); 5528 scanning = true; 5529 confirmBuffer = []; 5530 if (!reuse) applyTrackEnhancements();
5531 var startLoop = function() { if (scanning) (useNative ? nativeTick : zxingTick)(); }; 5532 if (video.readyState >= 2) startLoop(); 5533 else video.addEventListener('loadeddata', startLoop, { once: true }); 5534 }).catch(function() { 5535 alert('ã«ã¡ã©ãèµ·åã§ãã¾ããã§ãããã«ã¡ã©ã®æ¨©éã確èªãã¦ãã ããã'); 5536 }); 5537 } 5538 5539 // zxing-wasm ãé å»¶ importï¼ã¯ãªãã¯æã®ã¿ï¼ãwasm 㯠locateFile ã§èªãã¹ãå ã«åºå®ï¼CDN ãè¸ã¾ãªãï¼ 5540 function loadZxing() { 5541 useNative = false; 5542 CONFIRM_COUNT = 2; 5543 if (zxingRead) { startScan(); return; } 5544 import(ZXING_BASE + 'reader/index.js').then(function(mod){ 5545 if (mod.prepareZXingModule) { 5546 mod.prepareZXingModule({ 5547 overrides: { 5548 locateFile: function(path, prefix){ 5549 return /\.wasm$/.test(path) ? (ZXING_BASE + 'zxing_reader.wasm?v=' + ZXING_WASM_V) : (prefix + path); 5550 } 5551 } 5552 }); 5553 } 5554 zxingRead = mod.readBarcodes; 5555 startScan(); 5556 }).catch(function(){ 5557 alert('ã¹ãã£ãã¼ã®èªã¿è¾¼ã¿ã«å¤±æãã¾ãããéä¿¡ç°å¢ãã確èªãã ããã'); 5558 }); 5559 } 5560 5561 function begin() { 5562 // Android Chrome çã¯ãã¤ãã£ã BarcodeDetector ãåªå ï¼å³æã»ãã¦ã³ãã¼ãä¸è¦ï¼ã 5563 // iOS Safari 㯠BarcodeDetector éå¯¾å¿ â zxing-wasm ã«ãã©ã¼ã«ããã¯ã 5564 if ('BarcodeDetector' in window && window.BarcodeDetector.getSupportedFormats) { 5565 window.BarcodeDetector.getSupportedFormats().then(function(supported){ 5566 var want = ['ean_13', 'ean_8', 'upc_a', 'upc_e']; 5567 var fmts = want.filter(function(f){ return supported.indexOf(f) >= 0; }); 5568 if (fmts.length) { 5569 detector = new window.BarcodeDetector({ formats: fmts }); 5570 useNative = true; 5571 CONFIRM_COUNT = 2; 5572 startScan(); 5573 } else { 5574 loadZxing(); 5575 } 5576 }).catch(loadZxing); 5577 return; 5578 } 5579 loadZxing(); 5580 } 5581 5582 scanBtn.addEventListener('click', begin); 5583 if (inlineScanBtn) inlineScanBtn.addEventListener('click', begin); 5584 closeBtn.addEventListener('click', function(){ if (!sheetJustMoved()) closeScanner(); }); 5585 5586 // ã¢ã¼ãåæ¿ãã« 5587 if (modeToggle) modeToggle.addEventListener('click', function(ev){ 5588 var b = ev.target.closest('[data-mode]'); 5589 if (b) setMode(b.getAttribute('data-mode')); 5590 }); 5591 5592 // å±¥æ´ãªã¹ã: è¡ã¿ãã=ã·ã¼ãå表示 / Ã=åå¥åé¤ï¼å§è²ï¼ 5593 if (scoutList) { 5594 scoutList.addEventListener('click', function(ev){ 5595 if (sheetJustMoved()) return; // ã·ã¼ããéããç´å¾ã®ã´ã¼ã¹ã click ã§åãªã¼ãã³ããªã 5596 var del = ev.target.closest('[data-del]'); 5597 if (del) { 5598 ev.stopPropagation(); 5599 var j = del.getAttribute('data-del'); 5600 scoutHistory = scoutHistory.filter(function(e){ return e.jan !== j; }); 5601 saveScout(); renderScout(); 5602 return; 5603 } 5604 var row = ev.target.closest('.scout-item'); 5605 if (row) { pauseLoop(); openSheet(row.getAttribute('data-jan')); } 5606 }); 5607 scoutList.addEventListener('keydown', function(ev){ 5608 if (ev.key !== 'Enter' && ev.key !== ' ') return; 5609 var row = ev.target.closest && ev.target.closest('.scout-item'); 5610 if (row) { ev.preventDefault(); pauseLoop(); openSheet(row.getAttribute('data-jan')); } 5611 }); 5612 } 5613 if (scoutClear) scoutClear.addEventListener('click', function(){ 5614 if (!scoutHistory.length) return; 5615 if (!confirm('ã¹ãã£ã³å±¥æ´ããã¹ã¦åé¤ãã¾ããï¼')) return; 5616 scoutHistory = []; 5617 saveScout(); renderScout(); 5618 }); 5619 5620 // ã·ã¼ã: éãããå³ã¹ãã£ã³åéï¼ã«ã¡ã©ã¯çããã¾ã¾ï¼ï¼çµäº=é£ç¶ã¹ãã£ã³ãã¨éåº 5621 // éããç´å¾ 600ms ãææ¢ï¼å±¥æ´è¡ã¿ããã®ã´ã¼ã¹ã click ãã·ã¼ãåºé¨ãã¼ã«çå¼¾ããéæ¹å対çï¼ 5622 if (sheetBack) sheetBack.addEventListener('click', function(){ if (!sheetJustMoved()) closeSheet(true); }); 5623 if (sheetExit) sheetExit.addEventListener('click', function(){ if (!sheetJustMoved()) closeScanner(); }); 5624 if (sheetFrame) sheetFrame.addEventListener('load', function(){
5625 stripSheetChrome(); 5626 // iframe å ã§å¥ååã¸é·ç§»ããå ´åã«è¿½å¾ï¼åä¸ååå表示ã®å¤å®ããºã¬ãªãããã«ï¼ 5627 try { 5628 var m = (sheetFrame.contentWindow.location.pathname || '').match(/^\/dp\/(\d+)/); 5629 if (m) sheetFrame.setAttribute('data-jan', m[1]); 5630 } catch (e) {} 5631 pollSheetName(); 5632 }); 5633 5634 document.addEventListener('keydown', function(ev){ 5635 if (ev.key !== 'Escape') return; 5636 if (sheet && sheet.classList.contains('open')) closeSheet(true); 5637 else if (overlay.classList.contains('active')) closeScanner(); 5638 }); 5639 5640 // ãã¼ã¸é¢è±ã»ããã¯ã°ã©ã¦ã³ãåã§ã«ã¡ã©ã確å®ã«è§£æ¾ 5641 window.addEventListener('pagehide', releaseStream); 5642 document.addEventListener('visibilitychange', function(){ 5643 if (document.hidden && !overlay.classList.contains('active')) releaseStream(); 5644 }); 5645 5646 applyMode(); // ä¿åæ¸ã¿ã¢ã¼ãããã°ã«è¡¨ç¤ºã¸åæ 5647 5648 // ==================== å° æ³¨è¨ãã¼ã«ãããï¼viewport ã¯ã©ã³ã + ã¿ãã対å¿ï¼ ==================== 5649 // æ§ CSS .store-mark:hover::after ã¯ãããå³ç«¯ã§ãã¥ã¼ãã¼ãå¤ã«è¦åããã¢ãã¤ã«ã®ã¿ããã§ãæ®çããã 5650 // body ç´ä¸ã®åä¸è¦ç´ ã position:fixed ã§é ç½®ããå·¦å³ãç»é¢å ã«ã¯ã©ã³ããä¸ã«åã¾ããªããã°ä¸ã¸å転ã
5650ãã 5651 (function initStoreMarkTip() { 5652 let tipEl = null; 5653 let activeMark = null; 5654 const isTouch = window.matchMedia('(hover: none), (pointer: coarse)').matches; 5655 5656 function ensureTip() { 5657 if (tipEl) return tipEl; 5658 tipEl = document.createElement('div'); 5659 tipEl.className = 'store-mark-tip'; 5660 tipEl.setAttribute('role', 'tooltip'); 5661 document.body.appendChild(tipEl); 5662 return tipEl; 5663 } 5664 5665 function hideTip() { 5666 if (tipEl) tipEl.classList.remove('visible'); 5667 activeMark = null; 5668 } 5669 5670 function showTip(mark) { 5671 const text = mark.getAttribute('data-tip'); 5672 if (!text) return; 5673 const tip = ensureTip(); 5674 tip.textContent = text; 5675 activeMark = mark; 5676 // è¨æ¸¬ã®ãã䏿¦è¡¨ç¤ºï¼å·¦ä¸ã«ç½®ã㦠offsetWidth/Height ãåå¾ï¼ 5677 tip.style.left = '0px'; 5678 tip.style.top = '0px'; 5679 tip.classList.add('visible'); 5680 const margin = 12; 5681 const vw = document.documentElement.clientWidth; 5682 const vh = document.documentElement.clientHeight; 5683 const r = mark.getBoundingClientRect(); 5684 const tw = tip.offsetWidth; 5685 const th = tip.offsetHeight; 5686 // æ°´å¹³: ãããä¸å¿ã«åãããå·¦å³ãã¼ã¸ã³å ã«ã¯ã©ã³ãï¼ç»é¢å¤ã«åºããªãï¼ 5687 let left = r.left + r.width / 2 - tw / 2; 5688 left = Math.max(margin, Math.min(left, vw - tw - margin)); 5689 // åç´: æ¢å®ã¯ãããä¸ãåã¾ããªããã°ä¸ã¸å転 5690 let top = r.top - th - 8; 5691 let below = false; 5692 if (top < margin) { top = r.bottom + 8; below = true; } 5693 if (below && top + th > vh - margin) top = Math.max(margin, vh - th - margin); 5694 tip.style.left = Math.round(left) + 'px'; 5695 tip.style.top = Math.round(top) + 'px'; 5696 tip.classList.toggle('below', below); 5697 // ç¢å°ã¯ãããä¸å¿ãæãï¼tooltip å·¦ç«¯åºæºã»ä¸¡ç«¯ 12px ã§ã¯ã©ã³ãï¼ 5698 let arrowX = r.left + r.width / 2 - left; 5699 arrowX = Math.max(12, Math.min(arrowX, tw - 12)); 5700 tip.style.setProperty('--arrow-x', Math.round(arrowX) + 'px'); 5701 } 5702 5703 // PC: hover ã§è¡¨ç¤ºï¼ããªã²ã¼ã·ã§ã³ãmouseenter 㯠bubble ããªããã mouseover ã使ãï¼ 5704 document.addEventListener('mouseover', function(e) { 5705 if (isTouch) return; 5706 const mark = e.target.closest && e.target.closest('.store-mark'); 5707 if (mark) showTip(mark); 5708 }); 5709 document.addEventListener('mouseout', function(e) { 5710 if (isTouch) return; 5711 const mark = e.target.closest && e.target.closest('.store-mark'); 5712 if (mark && mark === activeMark) { 5713 const to = e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest('.store-mark'); 5714 if (to !== mark) hideTip(); 5715 } 5716 }); 5717 // ã¢ãã¤ã«/ã¿ãã: ãã°ã«è¡¨ç¤ºãå¤å´ã¿ããã§éãã 5718 document.addEventListener('click', function(e) { 5719 const mark = e.target.closest && e.target.closest('.store-mark'); 5720 if (mark) { 5721 e.stopPropagation(); 5722 if (mark === activeMark) hideTip(); 5723 else showTip(mark); 5724 } else { 5725 hideTip(); 5726 } 5727 }); 5728 document.addEventListener('keydown', function(e) { if (e.key === 'Escape') hideTip(); }); 5729 window.addEventListener('scroll', hideTip, true); 5730 window.addEventListener('resize', hideTip); 5731 })(); 5732 })(); 5733
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.