PageSourceSearch

https://xoigac.top/dist/js/app.bundle.js?v=mOWIwnGUFcWjsPNJLvW6NpHaA0wGjgZ4WYEv8xFjIMQ

js xoigac.top collected 2026-10-10 02:21:59 UTC 359,371 bytes, 1 lines download raw bytes

1function initPlayerMoreDropdown(){"function"==typeof window.initUnifiedPlayerMoreDropdown&&window.initUnifiedPlayerMoreDropdown()}function initDisableZoomAndLongPress(){const isEditableTarget=target=>!!target&&!(!target.closest||!target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""], [contenteditable]'));document.addEventListener("gesturestart",function(e){isEditableTarget(e.target)||e.preventDefault()},{passive:!1}),document.addEventListener("gesturechange",function(e){isEditableTarget(e.target)||e.preventDefault()},{passive:!1}),document.addEventListener("dblclick",function(e){isEditableTarget(e.target)||e.preventDefault()},{passive:!1});let lastTouchEndAt=0;document.addEventListener("touchend",function(e){if(isEditableTarget(e.target))return;const now=Date.now();now-lastTouchEndAt<=300&&e.preventDefault(),lastTouchEndAt=now},{passive:!1}),document.addEventListener("contextmenu",function(e){isEditableTarget(e.target)||e.preventDefault()})}function initNavigation(){const currentPath=window.location.pathname;document.querySelectorAll(".nav-link").forEach(link=>{link.getAttribute("href")===currentPath&&link.classList.add("active")}),initMobileMenuBehavior(),initMobileNavbarEnhancements()}
1function initMobileMenuBehavior(){const navbarCollapse=document.getElementById("navbarNav"),navbarToggler=document.querySelector(".navbar-toggler");function collapseMenu(){window.innerWidth>=992||!navbarCollapse.classList.contains("show")||"undefined"==typeof bootstrap||!bootstrap.Collapse||(bootstrap.Collapse.getInstance(navbarCollapse)||new bootstrap.Collapse(navbarCollapse,{toggle:!1})).hide()}navbarCollapse&&(navbarCollapse.querySelectorAll(".nav-link, .dropdown-item").forEach(link=>{link.addEventListener("click",function(e){setTimeout(collapseMenu,150)})}),document.addEventListener("click",function(e){if(window.innerWidth>=992)return;if(!navbarCollapse.classList.contains("show"))return;const clickedInMenu=navbarCollapse.contains(e.target),clickedToggler=navbarToggler&&navbarToggler.contains(e.target),clickedNavbar=e.target.closest(".navbar");clickedInMenu||clickedToggler||clickedNavbar&&clickedNavbar.contains(navbarCollapse)||collapseMenu()}),document.addEventListener("keydown",function(e){"Escape"===e.key&&collapseMenu()}))}
1function initMobileNavbarEnhancements(){const navbarCollapse=document.getElementById("navbarNav"),overlay=document.getElementById("mobileMenuOverlay"),mobileUserActions=document.querySelector(".mobile-user-actions");if(!navbarCollapse)return;const isMobile=()=>window.innerWidth<992;function closeHeaderDropdowns(){"undefined"!=typeof bootstrap&&bootstrap.Dropdown&&mobileUserActions?.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(toggle=>{bootstrap.Dropdown.getInstance(toggle)?.hide()}),mobileUserActions?.querySelectorAll(".dropdown-menu.show").forEach(menu=>{menu.classList.remove("show")}),mobileUserActions?.querySelectorAll(".user-menu.show").forEach(menu=>{menu.classList.remove("show")})}function hideNavbarCollapse(){"undefined"!=typeof bootstrap&&bootstrap.Collapse?(bootstrap.Collapse.getInstance(navbarCollapse)||new bootstrap.Collapse(navbarCollapse,{toggle:!1})).hide():(navbarCollapse.classList.remove("show"),setMenuOpen(!1))}function setMenuOpen(open){overlay?.classList.toggle("show",open),overlay?.setAttribute("aria-hidden",open?"false":"true"),document.body.classList.toggle("mobile-nav-open",open),open&&closeHeaderDropdowns()}navbarCollapse.addEventListener("show.bs.collapse",()=>{isMobile()&&setMenuOpen(!0)}),navbarCollapse.addEventListener("hidden.bs.collapse",()=>{setMenuOpen(!1)}),overlay?.addEventListener("click",()=>{navbarCollapse.classList.contains("show")&&hideNavbarCollapse()}),mobileUserActions?.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(toggle=>{toggle.addEventListener("show.bs.dropdown",()=>{isMobile()&&navbarCollapse.classList.contains("show")&&hideNavbarCollapse()})}),window.addEventListener("resize",()=>{isMobile()||(setMenuOpen(!1),closeHeaderDropdowns())})}function initSearch(){document.querySelectorAll(".search-box input, .mobile-search-box input").forEach(input=>{input.addEventListener("keypress",function(e){"Enter"===e.key&&(e.preventDefault(),(()=>{const query=(this.value||"").trim();if(!query)return;const matchRoute=window.__matchRoutePrefix||"/truc-tiep";window.location.href=`${matchRoute}?search=${encodeURIComponent(query)}`})())})}),initSmartSearchToggle();const searchIconBtn=document.getElementById("searchIconBtn"),searchPopup=document.getElementById("searchPopup"),searchCloseBtn=document.getElementById("searchCloseBtn"),popupSearchInput=document.getElementById("popupSearchInput");function goToSearchPage(query){const q=(query||"").trim();window.location.href=q?"/search?q="+encodeURIComponent(q):"/search"}searchIconBtn&&searchIconBtn.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),goToSearchPage("")}),searchPopup&&(searchCloseBtn&&searchCloseBtn.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),searchPopup.classList.remove("show")}),popupSearchInput&&popupSearchInput.addEventListener("keypress",function(e){"Enter"===e.key&&(e.preventDefault(),goToSearchPage(this.value))}),document.addEventListener("click",function(e){searchIconBtn&&!searchPopup.contains(e.target)&&e.target!==searchIconBtn&&searchPopup.classList.remove("show")}),document.addEventListener("keydown",function(e){"Escape"===e.key&&searchPopup.classList.contains("show")&&searchPopup.classList.remove("show")})),document.querySelectorAll(".btn-search-icon").forEach(btn=>{btn.addEventListener("click",function(){const wrapper=this.closest(".search-box, .mobile-search-box");if(!wrapper)return;const input=wrapper.querySelector("input"),q=(input?.value||"").trim();window.location.href=q?"/search?q="+encodeURIComponent(q):"/search"})}),document.querySelectorAll(".mobile-search-box .mobile-search-input").forEach(input=>{input.addEventListener("focus",function(){const q=(this.value||"").trim();window.location.href=q?"/search?q="+encodeURIComponent(q):"/search"}),input.addEventListener("click",function(){const q=(this.value||"").trim();window.location.href=q?"/search?q="+encodeURIComponent(q):"/search"})})}
1function initSmartSearchToggle(){const navbarContainer=document.querySelector(".navbar-container");function checkOverflow(){window.innerWidth>=992&&navbarContainer.classList.remove("mobile-overflow")}navbarContainer&&(checkOverflow(),window.addEventListener("resize",()=>{requestAnimationFrame(checkOverflow)}))}function initMatchCards(){document.querySelectorAll(".match-card").forEach(card=>{card.addEventListener("mouseenter",function(){this.style.transform="translateY(-5px)"}),card.addEventListener("mouseleave",function(){this.style.transform="translateY(0)"})}),document.querySelectorAll(".watch-btn").forEach(btn=>{btn.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();const matchCard=this.closest(".match-card");let matchUrl=null;if(matchCard){const bodyLink=matchCard.querySelector(".match-body-link");bodyLink&&bodyLink.href&&(matchUrl=bodyLink.href),!matchUrl&&"A"===matchCard.tagName&&matchCard.href&&(matchUrl=matchCard.href),matchUrl||(matchUrl=matchCard.getAttribute("data-match-url"))}matchUrl?window.spaRouter&&"function"==typeof window.spaRouter.openMatchDetail?window.spaRouter.openMatchDetail(matchUrl):window.location.href=matchUrl:console.warn("无法获取比赛URL")})})}function formatNumber(num){return window.i18n&&window.i18n.formatNumber?window.i18n.formatNumber(num):num.toString()}function formatTimeAgo(date){return window.i18n&&window.i18n.formatTimeAgo?window.i18n.formatTimeAgo(date):new Date(date).toLocaleDateString()}function showNotification(message,type="info"){const notification=document.createElement("div");let bgColor,borderColor,iconColor;switch(notification.className=`alert alert-${type} position-fixed`,type){case"success":bgColor="var(--bg-card)",borderColor="var(--accent-green)",iconColor="var(--accent-green)";break;case"danger":case"warning":bgColor="var(--bg-card)",borderColor="var(--accent-red)",iconColor="var(--accent-red)";break;default:bgColor="var(--bg-card)",borderColor="var(--accent-primary)",iconColor="var(--accent-primary)"}notification.style.cssText=`\n        top: 80px; \n        right: 20px; \n        z-index: 1060; \n        min-width: 300px; \n        animation: slideIn 0.3s ease;\n        background: ${bgColor};\n        border: 1px solid ${borderColor};\n        color: var(--text-primary);\n        box-shadow: var(--shadow-lg);\n        backdrop-filter: blur(10px);\n    `,notification.innerHTML=`\n        <div class="d-flex justify-content-between align-items-center">\n            <span style="flex: 1;">${message}</span>\n            <button type="button" class="btn-close btn-close-white" onclick="this.parentElement.parentElement.remove()" style="filter: brightness(0.8);"></button>\n        </div>\n    `,document.body.appendChild(notification),setTimeout(()=>notification.remove(),5e3)}function toggleFavorite(matchId){const favorites=JSON.parse(localStorage.getItem("favoriteMatches")||"[]"),index=favorites.indexOf(matchId);index>-1?(favorites.splice(index,1),showNotification(window.L?.T?window.L.T("match.unfavorited"):"已取消收藏","warning")):(favorites.push(matchId),showNotification(window.L?.T?window.L.T("match.favorited"):"收藏成功!","success")),localStorage.setItem("favoriteMatches",JSON.stringify(favorites))}function showReportDialog(matchId){const t=key=>window.reportI18n&&window.reportI18n[key]?window.reportI18n[key]:key,modalHtml=`\n        <div class="modal fade" id="reportModal" tabindex="-1" aria-labelledby="reportModalLabel" aria-hidden="true">\n            <div class="modal-dialog modal-dialog-centered">\n                <div class="modal-content" style="background: #0a0a0a; border: 1px solid var(--border-color); color: var(--text-primary); box-shadow: var(--shadow-lg);">\n                    <div class="modal-header" style="border-bottom: 1px solid var(--border-color); background: #0f0f0f;">\n                        <h5 class="modal-title" id="reportModalLabel">\n                            <i class="fas fa-exclamation-triangle me-2" style="color: var(--accent-orange);"></i>\n                            ${t("dialogTitle")}\n                        </h5>\n                        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>\n                    </div>\n                    <div class="modal-body">\n                        <form id="reportForm">\n                            <div class="mb-3">\n                                <label for="reportType" class="form-label" style="color: var(--text-secondary); font-weight: 500;">${t("problemType")}</label>\n                                <select class="form-select" id="reportType" style="background: #0f0f0f; border: 1px solid var(--border-color); color: var(--text-primary);">\n                                    <option value="video">${t("videoProblem")}</option>\n                                    <option value="audio">${t("audioProblem")}</option>\n                                    <option value="loading">${t("slowLoading")}</option>
1\n                                    <option value="quality">${t("qualityProblem")}</option>\n                                    <option value="other">${t("other")}</option>\n                                </select>\n                            </div>\n                            <div class="mb-3">\n                                <label for="reportDescription" class="form-label" style="color: var(--text-secondary); font-weight: 500;">${t("description")}</label>\n                                <textarea class="form-control" id="reportDescription" rows="3" \n                                    placeholder="${t("descriptionPlaceholder")}"\n                                    style="background: #0f0f0f; border: 1px solid var(--border-color); color: var(--text-primary); resize: vertical;"></textarea>\n                                <style>\n                                    #reportDescription::placeholder {\n                                        color: var(--text-muted) !important;\n                                        opacity: 1 !important;\n                                    }\n                                    #reportDescription::-webkit-input-placeholder {\n                                        color: var(--text-muted) !important;\n                                        opacity: 1 !important;\n                                    }\n                                    #reportDescription::-moz-placeholder {\n                                        color: var(--text-muted) !important;\n                                        opacity: 1 !important;\n                                    }\n                                    #reportDescription:-ms-input-placeholder {\n                                        color: var(--text-muted) !important;\n                                        opacity: 1 !important;\n                                    }\n                                </style>\n                            </div>\n                            <input type="hidden" id="reportMatchId" value="${matchId}">\n                        </form>\n                    </div>\n                    <div class="modal-footer" style="border-top: 1px solid var(--border-color); background: #0f0f0f;">\n                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" style="background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-secondary);">${t("cancel")}</button>\n                        <button type="button" class="btn btn-warning" onclick="submitReport()" style="background: var(--accent-orange); border-color: var(--accent-orange);">\n                            <i class="fas fa-paper-plane me-1"></i>${t("submitReport")}\n                        </button>\n                    </div>\n                </div>\n            </div>\n        </div>\n    `,existingModal=document.getElementById("reportModal");existingModal&&existingModal.remove(),document.body.insertAdjacentHTML("beforeend",modalHtml);const modalElement=document.getElementById("reportModal");modalElement&&"undefined"!=typeof bootstrap&&bootstrap.Modal?new bootstrap.Modal(modalElement,{backdrop:!0,keyboard:!0,focus:!0}).show():console.error("Bootstrap Modal 未加载或 reportModal 元素不存在")}async function submitReport(){const t=key=>window.reportI18n&&window.reportI18n[key]?window.reportI18n[key]:key,matchId=document.getElementById("reportMatchId").value,reportType=document.getElementById("reportType").value,description=document.getElementById("reportDescription").value.trim();if(!description)return void showNotification(t("validationError"),"warning");const submitBtn=document.querySelector("#reportModal .btn-warning"),originalText=submitBtn.innerHTML;submitBtn.disabled=!0,submitBtn.innerHTML=`<i class="fas fa-spinner fa-spin me-1"></i>${t("submitting")}`;try{const response=await fetch("/api/report",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({matchId:parseInt(matchId),reportType:reportType,description:description})}),result=await response.json();if(response.ok&&result.success){const modalElement=document.getElementById("reportModal");if(modalElement&&"undefined"!=typeof bootstrap&&bootstrap.Modal){const modal=bootstrap.Modal.getInstance(modalElement);modal&&modal.hide()}showNotification(t("reportSubmitted"),"success")}else showNotification(result.message||t("submitFailed"),"danger")}catch(error){console.error("提交报告失败:",error),showNotification(t("networkError"),"danger")}finally{submitBtn.disabled=!1,submitBtn.innerHTML=originalText}}function setReminder(matchId,matchTime,matchInfo={}){const reminders=JSON.parse(localStorage.getItem("matchReminders")||"[]");reminders.find(r=>r.id===matchId)||(reminders.push({id:matchId,time:matchTime,homeTeam:matchInfo.homeTeam||"",awayTeam:matchInfo.awayTeam||"",homeTeamLogo:matchInfo.homeTeamLogo||"",awayTeamLogo:matchInfo.awayTeamLogo||"",league:matchInfo.league||"",leagueLogo:matchInfo.leagueLogo||""}),localStorage.setItem("matchReminders",JSON.stringify(reminders)),showNotification(window.L?.T?window.L.T("home.reminderSet"):"提醒设置成功!比赛开始前会通知您","success"))}function checkReminders(){const reminders=JSON.parse(localStorage.getItem("matchReminders")||"[]"),now=(new Date).getTime(),triggeredIndices=[];
1if(reminders.forEach((reminder,index)=>{const diff=new Date(reminder.time).getTime()-now;diff>0&&diff<=9e5&&(void 0!==window.MatchLiveHub&&window.MatchLiveHub.showMatchNotification?window.MatchLiveHub.showMatchNotification("reminder",{matchId:reminder.id,homeTeam:reminder.homeTeam||"",awayTeam:reminder.awayTeam||"",homeTeamLogo:reminder.homeTeamLogo||"",awayTeamLogo:reminder.awayTeamLogo||"",league:reminder.league||"",leagueLogo:reminder.leagueLogo||""},{showMuteButton:!1,isReminder:!0}):showNotification(window.L?.T?window.L.T("home.matchReminder"):"您关注的比赛即将开始!","warning"),triggeredIndices.push(index))}),triggeredIndices.length>0){for(let i=triggeredIndices.length-1;i>=0;i--)reminders.splice(triggeredIndices[i],1);localStorage.setItem("matchReminders",JSON.stringify(reminders))}}!function(){let audioUnlocked=!1;function unlockAudio(){if(!audioUnlocked){audioUnlocked=!0;try{window.audioContext&&"suspended"===window.audioContext.state&&window.audioContext.resume();const ctx=new(window.AudioContext||window.webkitAudioContext),oscillator=ctx.createOscillator(),gainNode=ctx.createGain();gainNode.gain.value=0,oscillator.connect(gainNode),gainNode.connect(ctx.destination),oscillator.start(0),oscillator.stop(.001)}catch(e){console.warn("[AudioUnlock] AudioContext 解锁失败:",e)}!function removeAllListeners(){interactionEvents.forEach(eventType=>{document.removeEventListener(eventType,handleInteraction,{capture:!0})})}()}}const interactionEvents=["click","pointerdown","touchstart","keydown"];function handleInteraction(e){unlockAudio()}function addAllListeners(){interactionEvents.forEach(eventType=>{document.addEventListener(eventType,handleInteraction,{once:!0,passive:!0,capture:!0})})}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",addAllListeners):addAllListeners(),window.unlockGlobalAudio=unlockAudio,window.isAudioUnlocked=()=>audioUnlocked}(),document.addEventListener("DOMContentLoaded",function(){initDisableZoomAndLongPress(),initNavigation(),initSearch(),initMatchCards(),initLivePlayer(),initPlayerMoreDropdown(),initDetailCloudflareWhepEventListeners(),initHomeCloudflareWhepEventListeners()});let hlsInstance=null;window.getDetailHlsInstance=()=>hlsInstance;let playerRetryCount=0;const MAX_RETRY_COUNT=3,LOAD_TIMEOUT_MS=3e4;let playerLoadTimeout=null,livekitRoom=null,webrtcConnected=!1,isPlayingInProgress=!1,detailStreamPollingTimer=null,detailIsStreamPlaying=!1;const DETAIL_STREAM_POLL_INTERVAL=5e3;let lastMatchId=null,lastStreamingType=null,publisherPollingTimer=null,publisherTimeoutTimer=null,isStreamStartingWait=!1;const PUBLISHER_POLL_INTERVAL=3e3,PUBLISHER_TIMEOUT=2e4;function showStreamStartingOverlay(message){const container=document.getElementById("livePlayerContainer");if(!container)return;hideDetailPlayOverlay(),hideErrorOverlay(),hideLoadingMessage(),isStreamStartingWait=!0;const startingText=message||document.body.getAttribute("data-stream-starting-text")||"首次启动直播,请稍等片刻",connectingText=document.body.getAttribute("data-connecting-stream-text")||"正在连接直播源...";let overlay=document.getElementById("streamStartingOverlay");overlay||(overlay=document.createElement("div"),overlay.id="streamStartingOverlay",overlay.className="stream-starting-overlay",overlay.style.cssText="\n            position: absolute;\n            inset: 0;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            background: rgba(0,0,0,0.95);\n            z-index: 30;\n            color: #fff;\n            font-size: 16px;\n            text-align: center;\n            padding: 20px;\n        ",container.appendChild(overlay)),overlay.innerHTML=`\n        <div style="margin-bottom: 15px;">\n            <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n                <circle cx="12" cy="12" r="10" stroke="#3B82F6" stroke-width="2" opacity="0.3"/>\n                <path d="M12 2a10 10 0 0 1 10 10" stroke="#3B82F6" stroke-width="2" stroke-linecap="round">\n                    <animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indef
1inite"/>\n                </path>\n            </svg>\n        </div>\n        <div style="font-weight: 500; margin-bottom: 8px;">${startingText}</div>\n        <div style="font-size: 13px; color: #9CA3AF;">${connectingText}</div>\n    `,overlay.style.display="flex"}function hideStreamStartingOverlay(){const overlay=document.getElementById("streamStartingOverlay");overlay&&(overlay.style.display="none"),isStreamStartingWait=!1}function stopPublisherPolling(){publisherPollingTimer&&(clearInterval(publisherPollingTimer),publisherPollingTimer=null),publisherTimeoutTimer&&(clearTimeout(publisherTimeoutTimer),publisherTimeoutTimer=null)}function startPublisherPolling(matchId){stopPublisherPolling(),window.PcPlaybackVisibility?.isPageHidden()&&window.PcPlaybackVisibility?.shouldManageCurrentStream()||(showStreamStartingOverlay(),publisherTimeoutTimer=setTimeout(()=>{stopPublisherPolling(),hideStreamStartingOverlay(),"function"==typeof isDetailPlayerActuallyPlaying&&isDetailPlayerActuallyPlaying()?hideDetailErrorOverlay():showErrorOverlay(document.body.getAttribute("data-stream-timeout-text")||(window.L?.T?window.L.T("match.player.streamTimeout"):"直播启动超时,请稍后重试"),!0)},2e4),publisherPollingTimer=setInterval(async()=>{if(!window.PcPlaybackVisibility?.isPageHidden()||!window.PcPlaybackVisibility?.shouldManageCurrentStream())try{const response=await fetch(`/match/stream-check/${matchId}`);if(!response.ok)return void console.warn("[PublisherPolling] 检查失败:",response.status);const result=await response.json(),publishersWhileCheck=Number(result.publishers??0);if(result&&(!0===result.notInTimeWindow||!1===result.canStart)&&publishersWhileCheck<=0&&!0!==result.success){stopPublisherPolling(),hideStreamStartingOverlay();const msgKey=result.windowMessage||"player.notLiveYet";return void showErrorOverlay(window.L?.T?window.L.T(msgKey):result.message||"比赛暂未开始直播",!1)}if(Number(result.publishers??0)>0){if(window.PcPlaybackVisibility?.deferAutoPlay())return;stopPublisherPolling(),hideStreamStartingOverlay(),loadWebRtcStream(matchId,!0).then(()=>{detailIsStreamPlaying=!0}).catch(e=>{console.error("[PublisherPolling] 播放失败:",e)})}}catch(e){console.error("[PublisherPolling] 轮询异常:",e)}},3e3))}function translateServerMessage(message){return message&&window.L?.T?"string"==typeof message&&message.startsWith("player.")?window.L.T(`match.${message}`):window.L.T(message):message}const DETAIL_SESSION_USER_PLAYED="detail_user_played",hasDetailUserInitiatedPlay=()=>{try{return"1"===sessionStorage.getItem("detail_user_played")}catch(e){return!1}},setDetailUserInitiatedPlay=()=>{try{sessionStorage.setItem("detail_user_played","1")}catch(e){}};function showDetailPlayOverlay(onClick){const container=document.getElementById("livePlayerContainer");if(!container)return;let overlay=document.getElementById("detailPlayOverlay");overlay||(overlay=document.createElement("div"),overlay.id="detailPlayOverlay",overlay.className="detail-play-overlay",overlay.innerHTML='\n            <button type="button" class="detail-play-btn">\n                <span class="detail-play-icon">▶</span>\n                <span class="detail-play-text">Play</span>\n            </button>\n        ',container.appendChild(overlay)),overlay.style.position="absolute",overlay.style.inset="0",overlay.style.alignItems="center",overlay.style.justifyContent="center",overlay.style.background="rgba(0,0,0,1)",overlay.style.backdropFilter="",overlay.style.zIndex="30",overlay.style.display="flex";const btn=overlay.querySelector(".detail-play-btn");btn&&(btn.style.display="inline-flex",btn.style.alignItems="center",btn.style.gap="10px",btn.style.padding="12px 18px",btn.style.borderRadius="12px",btn.style.border="1px solid rgba(255,255,255,0.25)",btn.style.background="rgba(15,23,42,0.75)",btn.style.color="#fff",btn.style.fontSize="14px",btn.style.cursor="pointer",btn.onclick=e=>{e.preventDefault(),e.stopPropagation(),"function"==typeof onClick&&onClick()})}function hideDetailPlayOverlay(){const overlay=document.getElementById("detailPlayOverlay");overlay&&(overlay.style.display="none")}function showDetailPlayButton(){const container=document.getElementById("livePlayerContainer");container&&showDetailPlayOverlay(()=>{setDetailUserInitiatedPlay(),startDetailPlayback(container.dataset.matchI
1d,container.dataset.streamingType)})}function startDetailPlayback(matchId,streamingType){const video=document.getElementById("livePlayer");video&&!video.muted&&(video.muted=!0);const container=document.getElementById("livePlayerContainer"),identifier=container?.getAttribute("data-identifier");hideDetailPlayOverlay(),showLoadingMessage(),loadStreamWithRetry(matchId,streamingType,identifier)}function initLivePlayer(){const video=document.getElementById("livePlayer");if(!video)return;const container=video.closest(".live-player-container"),clearLoading=()=>{container&&container.classList.remove("loading")};document.querySelector('meta[name="match-has-video"]'),document.querySelector('meta[name="match-has-stream-sources"]'),document.querySelector('meta[name="match-has-streamer-task"]'),document.querySelector('meta[name="match-streaming-type"]'),video.addEventListener("playing",()=>{hidePlayButtonOverlay(video),hideDetailPlayOverlay(),hideStreamStartingOverlay(),hideDetailErrorOverlay(),hideErrorOverlay(),hideLoadingMessage(),detailIsStreamPlaying=!0,stopPublisherPolling()}),video.addEventListener("pause",()=>{const container=document.getElementById("livePlayerContainer");"true"!==container?.dataset?.userPausedLive&&detailIsStreamPlaying&&(video.srcObject||video.src)&&!video.ended&&video.play().catch(()=>{})}),video.addEventListener("play",()=>{setTimeout(()=>{video.paused||video.ended||hidePlayButtonOverlay(video)},200)}),video.volume=(()=>{const v=parseFloat(localStorage.getItem("live_player_volume")||"");return Number.isFinite(v)?Math.min(1,Math.max(0,v)):.8})(),video.muted=(()=>{const m=localStorage.getItem("live_player_muted");return null===m||"true"===m})(),null===localStorage.getItem("live_player_muted")&&(video.muted=!0),video.addEventListener("volumechange",()=>{try{localStorage.setItem("live_player_volume",String(video.volume)),localStorage.setItem("live_player_muted",String(video.muted))}catch{}}),video.addEventListener("play",()=>{window.dispatchEvent(new Event("livePlayerStarted")),hideErrorOverlay(),hidePlayButtonOverlay(video),hideDetailPlayOverlay(),clearLoading()}),video.addEventListener("playing",clearLoading),video.addEventListener("canplay",clearLoading),video.addEventListener("loadeddata",clearLoading),video.addEventListener("loadedmetadata",clearLoading),function cleanupPlayButtons(){document.querySelectorAll("button").forEach(btn=>{(btn.textContent.includes("点击播放视频")||btn.textContent.includes("点击播放"))&&btn.remove()})}(),autoOpenLivePlayer(),initPictureInPicture(video)}let isPipActive=!1,isPipUserDisabled=!1,hasUserGesture=!1,detailPiPManager=null;function initPictureInPicture(video){if(!video||!document.pictureInPictureEnabled)return;if("undefined"!=typeof PersistentPlaybackManager)return;if("undefined"!=typeof PiPManager&&!detailPiPManager)return detailPiPManager=new PiPManager(video,{enableDesktopAutoPiP:!0,enableMobileAutoPiP:!0,onPiPEnter:()=>{isPipActive=!0},onPiPExit:()=>{isPipActive=!1}}),detailPiPManager.initialize(),void(window.detailPiPManager=detailPiPManager);const playerSection=document.getElementById("livePlayerSection");if(!playerSection)return;const enableGesture=()=>{hasUserGesture||(hasUserGesture=!0)};["click","mousedown","mouseup","mousemove","touchstart","touchend","touchmove","keydown","keyup","keypress","wheel","scroll"].forEach(event=>{document.addEventListener(event,enableGesture,{once:!0,passive:!0})}),video.addEventListener("enterpictureinpicture",()=>{isPipActive=!0}),video.addEventListener("leavepictureinpicture",()=>{isPipActive=!1,playerSection.getBoundingClientRect().bottom<0&&(isPipUserDisabled=!0)}),new IntersectionObserver(entries=>{const entry=entries[0];video.paused||video.ended||!detailIsStreamPlaying||hasUserGesture&&(video.readyState<1||(entry.isIntersecting?(isPipActive&&document.exitPictureInPicture().catch(()=>{}),isPipUserDisabled=!1):isPipActive||isPipUserDisabled||video.requestPictureInPicture().catch(e=>{e.message})))},{threshold:0,rootMargin:"0px"}).observe(playerSection)}async function startStream(matchId){try{const checkResponse=await fetch(`/match/stream-check/${matchId}`),checkResult=await checkResponse.json();if(checkResult&&(!0===checkResult.notInTimeWindow||!1===checkResult.canStart)&&Number(checkResult.publishers??0)<=0&&!0!==checkResult.success){const msgKey=checkResult.windowMessage||"player.notLiveYet";return{success:!1,status:"not_in_window",message:window.L?.T?window.L.T("match."+msgKey):"比赛暂未开始直播",notInTimeWindow:!0}}if(!0===checkResult?.allSourcesFailed||"player.allSourcesFailed"===checkResult?.message)return{success:!1,status:"failed",message:"player.allSourcesFailed",allSourcesFailed:!0};const response=await fetch(`/match/start-stream/${matchId}`,{method:"POST"});return await response.json()}catch(error){return console.error("[Player] 启动流失败:",error),{success:!1,status:"error",message:"无法启动流"}}}async function checkStreamStatus(matchId){try{const response=await fetch(`/match/stream-status/${matchId}`);return await response.json()}catch(error){return console.error("流状态检查失败:",error),{success:!1,status:"error",message:window.L?.T?window.L.T("match.player.streamCheckFailed"):"无法检查流状态"}}}async function checkStreamStatus(matchId){try{const response=await fetch(`/match/stream-check/${matchId}`);return await response.json()}catch(error){return console.error("流状态检查失败:",error),{success:!1,status:"error",message:window.L?.T?window.L.T("match.player.streamCheckFailed"):"无法检查流状态"}}}async function loadHlsStream(url,matchId=null,options={}){const{enableQualitySelector:enableQualitySelector=!1,skipStartStream
1:skipStartStream=!1,hlsVariants:hlsVariants=null}=options,video=document.getElementById("livePlayer");if(!video)return;const container=video.closest(".live-player-container");"function"==typeof hideHlsQualitySelector&&hideHlsQualitySelector("detailHlsQualitySelector");const setLoading=isLoading=>{container&&(isLoading?container.classList.add("loading"):container.classList.remove("loading"))},tryAutoplay=()=>(video.muted||(video.muted=!0),video.play().catch(()=>Promise.reject()));if(hlsInstance&&(hlsInstance.destroy(),hlsInstance=null),hideErrorOverlay(),playerRetryCount=0,setLoading(!0),matchId&&!skipStartStream){const startResult=await startStream(matchId);if(!startResult.success||"running"!==startResult.status&&"ready"!==startResult.status){const errorMsg=startResult.message||"直播流未启动或状态异常";return console.error("HLS流启动失败:",errorMsg),showErrorOverlay(errorMsg,!0),void setLoading(!1)}}if(setLoadTimeout(),"undefined"!=typeof Hls&&Hls.isSupported())hlsInstance=new Hls({enableWorker:!0,startLevel:-1,lowLatencyMode:!0,liveSyncDurationCount:1.5,liveMaxLatencyDurationCount:2,liveDurationInfinity:!0,maxLiveSyncPlaybackRate:1.2,maxBufferLength:8,maxMaxBufferLength:10,maxBufferHole:.3,backBufferLength:5,maxFragLookUpTolerance:.25,fragLoadingMaxRetry:6,manifestLoadingMaxRetry:4,levelLoadingMaxRetry:4}),hlsInstance.loadSource(url),hlsInstance.attachMedia(video),hlsInstance.on(Hls.Events.MANIFEST_PARSED,()=>{clearLoadTimeout(),setLoading(!1),hideErrorOverlay(),enableQualitySelector&&"function"==typeof setupMuxHlsQualitySelector&&setupMuxHlsQualitySelector({hls:hlsInstance,video:video,masterUrl:url,variants:hlsVariants,selectorId:"detailHlsQualitySelector",storageKey:"mux_detail_hls_quality_level"}),tryAutoplay().then(()=>{hideErrorOverlay()}).catch(()=>{})}),hlsInstance.on(Hls.Events.FRAG_LOADED,()=>{clearLoadTimeout(),setLoading(!1),hideErrorOverlay(),video.paused&&tryAutoplay().then(()=>{hideErrorOverlay()})}),hlsInstance.on(Hls.Events.ERROR,(event,data)=>
1{if(data.fatal)switch(data.type){case Hls.ErrorTypes.NETWORK_ERROR:console.warn("HLS网络错误,尝试恢复..."),playerRetryCount<3?(playerRetryCount++,setTimeout(()=>hlsInstance&&hlsInstance.startLoad(),2e3)):(showErrorOverlay(document.body.getAttribute("data-playback-failed-text")||"播放失败",!0),setLoading(!1));break;case Hls.ErrorTypes.MEDIA_ERROR:console.warn("HLS媒体错误,尝试恢复..."),hlsInstance.recoverMediaError();break;default:console.error("HLS致命错误:",data),showErrorOverlay(document.body.getAttribute("data-playback-failed-text")||"播放失败",!0),setLoading(!1)}});else if(video.canPlayType("application/vnd.apple.mpegurl")){const selector=enableQualitySelector&&"function"==typeof setupMuxHlsQualitySelector?await setupMuxHlsQualitySelector({video:video,masterUrl:url,variants:hlsVariants,selectorId:"detailHlsQualitySelector",storageKey:"mux_detail_hls_quality_level"}):null;selector&&-1!==selector.currentLevel||(video.src=url),video.addEventListener("loadedmetadata",()=>{if(clearLoadTimeout(),video.seekable&&video.seekable.length>0){const liveEdge=video.seekable.end(video.seekable.length-1),startPosition=Math.max(0,liveEdge-3);video.currentTime=startPosition,startPosition.toFixed(1),liveEdge.toFixed(1)}setLoading(!1),tryAutoplay().then(()=>{hideErrorOverlay()}).catch(()=>{})},{once:!0})}else showErrorOverlay(document.body.getAttribute("data-unsupported-text")||"您的浏览器不支持此播放器",!1),setLoading(!1)}function setLoadTimeout(){clearLoadTimeout(),playerLoadTimeout=setTimeout(()=>{const video=document.getElementById("livePlayer"),hasSrc=video&&(video.src||video.srcObject),isLoading=video&&video.networkState>=1,hasStartedPlaying=video&&(video.currentTime>0||!video.paused);if(hasSrc&&isLoading&&!hasStartedPlaying){console.warn("视频加载超时(20秒),显示错误提示"),showErrorOverlay(document.body.getAttribute("data-playback-failed-text")||"播放超时,流可能存在问题",!0);const container=video.closest(".live-player-container");container&&container.classList.remove("loading")}},2e4)}function clearLoadTimeout(){playerLoadTimeout&&(clearTimeout(playerLoadTimeout),playerLoadTimeout=null)}function showLoadingMessage(){let video=document.getElementById("livePlayer");if(video||(video=document.getElementById("focusPlayer")),video||(video=document.querySelector("video")),!video)return void console.warn("[LoadingMessage] 找不到视频元素,无法显示加载动画");video.id;const loadingText=document.body.dataset.loadingText||"正在获取资源,请稍等几秒钟...",hintText=document.body.dataset.loadingHintText||"如果视频无法播放或卡顿请尝试刷新";let loadingOverlay=document.querySelector(".loading-message-overlay");loadingOverlay||(loadingOverlay=document.createElement("div"),loadingOverlay.className="loading-message-overlay",video.parentElement.appendChild(loadingOverlay)),loadingOverlay.innerHTML=`\n        <div class="loading-content">\n            <div class="loading-spinner">\n                <div class="loading-spinner-dot"></div>\n            </div>\n            <div class="loading-text">${loadingText}</div>\n            <div class="loading-hint">${hintText}</div>\n            <div class="loading-progress"></div>\n        </div>\n    `}function hideLoadingMessage(){const loadingOverlay=document.querySelector(".loading-message-overlay");loadingOverlay&&loadingOverlay.remove()}let globalRetryManager={retryCount:0,maxRetries:10,baseDelay:3e3,maxDelay:1e4,retryTimer:null,totalRetries:0,maxTotalRetries:20,getRetryDelay(){return Math.min(this.baseDelay*Math.pow(2,this.retryCount),this.maxDelay)+1e3*Math.random()},reset(){this.retryCount=0,this.retryTimer&&(clearTimeout(this.retryTimer),this.retryTimer=null),this.totalRetries=0},scheduleRetry(callback){if(this.totalRetries>=this.maxTotalRetries)return console.error("[RetryManager] ❌ 达到绝对最大重试次数,停止重试,显示错误"),this.showMaxRetriesError(),!1;if(this.retryCount>=this.maxRetries)return!1;const delay=this.getRetryDelay();return this.retryCount++,this.totalRetries++,this.retryCount,this.totalRetries,Math.round(delay),this.retryTimer=setTimeout(()=>{callback()},delay),!0},showMaxRetriesError(){if(hideLoadingMessage(),!document.getElementById("livePlayer"))return;let overlay=document.getElementById("playerMaxRetriesOverlay");if(!overlay){overlay=document.createElement("div"),overlay.id="playerMaxRetriesOverlay",overlay.className="player-error-overlay";
1const container=document.querySelector(".live-player-container");container&&container.appendChild(overlay)}const errorText=document.body.getAttribute("data-playback-error-text")||"播放失败",retryText=document.body.getAttribute("data-retry-text")||"重试";overlay.innerHTML=`\n            <div class="error-content-wrapper">\n                <div class="error-card">\n                    <div class="error-icon-container">\n                        <svg class="error-icon-svg" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n                            <circle cx="12" cy="12" r="10" stroke="#EF4444" stroke-width="1.5" opacity="0.3"/>\n                            <path d="M12 8v4M12 16v1" stroke="#EF4444" stroke-width="2" stroke-linecap="round"/>\n                        </svg>\n                    </div>\n                    <h3 class="error-title">${errorText}</h3>\n                    <p class="error-message">多次尝试后仍无法播放,请稍后再试</p>\n                    <button class="error-retry-btn" onclick="manualRetryPlayback()">\n                        <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n                            <path d="M4 12a8 8 0 0 1 14.93-4M20 12a8 8 0 0 1-14.93 4M4 8v4h4M20 16v-4h-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                        </svg>\n                        ${retryText}\n                    </button>\n                </div>\n            </div>\n        `,overlay.classList.add("show")}};async function checkLiveStatusFromRedis(matchId){try{const response=await fetch(`/api/live-status/${matchId}`);return(await response.json()).isLive||!1}catch(error){return console.error("[LiveStatus] 检查直播状态失败:",error),!1}}async function handlePlaybackError(message,matchId){const video=document.getElementById("livePlayer");if(video&&hidePlayButtonOverlay(video),!window.hasTriedPlayback)return window.hasTriedPlayback=!0,void showLoadingWithRetry(matchId);await checkLiveStatusFromRedis(matchId)?showLoadingWithRetry(matchId):showNotLiveMessage()}function getOrCreateDetailErrorOverlay(){const container=document.querySelector(".live-player-container");if(!container)return null;let overlay=document.getElementById("playerNotLiveOverlay");return overlay||(overlay=document.createElement("div"),overlay.id="playerNotLiveOverlay",overlay.className="player-error-overlay",container.appendChild(overlay)),overlay}function hideDetailErrorOverlay(){const overlay=document.getElementById("playerNotLiveOverlay");overlay&&(overlay.classList.remove("show"),overlay.remove());const legacyOverlay=document.getElementById("playerErrorOverlay");legacyOverlay&&legacyOverlay.remove();const container=document.getElementById("livePlayerContainer");container&&(container.classList.remove("has-not-live-overlay"),container.querySelectorAll(".focus-not-live-overlay, .focus-error-overlay, .player-error-overlay").forEach(el=>el.remove()))}function showNotLiveMessage(matchId=null){if(window.isSwitchingStreamer)return;if("function"==typeof isDetailPlayerActuallyPlaying&&isDetailPlayerActuallyPlaying())return void hideDetailErrorOverlay();if(hideLoadingMessage(),hideDetailErrorOverlay(),!matchId){const urlMatch=window.location.pathname.match(/id(\d+)$/);urlMatch&&(matchId=urlMatch[1])}const overlay=getOrCreateDetailErrorOverlay();if(!overlay)return;const notLiveText=document.body.getAttribute("data-not-live-text")||"直播未开始";let retryBtnText="重试";try{if(window.L&&window.L.T){const translated=window.L.T("match.player.retry")||window.L.T("common.retry");!translated||translated.startsWith("match.")||translated.startsWith("common.")||(retryBtnText=translated)}}catch(e){console.warn("[DetailPlayer] 翻译加载失败,使用默认文本")}overlay.innerHTML=`\n        <div class="error-content-wrapper">\n            <div class="error-card error-card-inline">\n                <div class="error-message-row">\n                    <svg class="error-icon-svg-inline" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n                        <circle cx="12" cy="12" r="10" stroke="#309e58" stroke-width="1.5" fill="none" opacity="0.4"/>\n                        <path d="M12 8v5" stroke="#86efac" stroke-width="2.5" stroke-linecap="round"/>\n                        <circle cx="12" cy="16" r="1.2" fill="#86efac"/>\n                    </svg>\n                    <h3 class="error-title-inline" onmousedown="event.preventDefault()">${notLiveText}</h3>\n                </div>\n                <button class="error-retry-btn error-retry-btn-inline" onclick="retryDetailPlayback()" type="button">
1\n                    <svg class="retry-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n                        <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                        <path d="M3 3v5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                        <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                        <path d="M16 21v-5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                    </svg>\n                    <span>${retryBtnText}</span>\n                </button>\n            </div>\n        </div>\n    `,overlay.classList.add("show")}function showDetailPlayerError(message,showRetryButton=!0){if(window.isSwitchingStreamer)return;if("function"==typeof isDetailPlayerActuallyPlaying&&isDetailPlayerActuallyPlaying())return void hideDetailErrorOverlay();hideLoadingMessage(),hideDetailErrorOverlay();const overlay=getOrCreateDetailErrorOverlay();if(!overlay)return;let retryBtnText="重试";try{if(window.L&&window.L.T){const translated=window.L.T("match.player.retry")||window.L.T("common.retry");!translated||translated.startsWith("match.")||translated.startsWith("common.")||(retryBtnText=translated)}}catch(e){console.warn("[DetailPlayer] 翻译加载失败,使用默认文本")}overlay.innerHTML=`\n        <div class="error-content-wrapper">\n            <div class="error-card error-card-inline">\n                <div class="error-message-row">\n                    <svg class="error-icon-svg-inline" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n                        <circle cx="12" cy="12" r="10" stroke="#309e58" stroke-width="1.5" fill="none" opacity="0.4"/>\n                        <path d="M12 8v5" stroke="#86efac" stroke-width="2.5" stroke-linecap="round"/>\n                        <circle cx="12" cy="16" r="1.2" fill="#86efac"/>\n                    </svg>\n                    <h3 class="error-title-inline" onmousedown="event.preventDefault()">${message}</h3>\n                </div>\n                <button class="error-retry-btn error-retry-btn-inline" onclick="retryDetailPlayback()" type="button">\n                    <svg class="retry-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n                        <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                        <path d="M3 3v5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                        <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                        <path d="M16 21v-5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\n                    </svg>\n                    <span>${retryBtnText}</span>\n                </button>\n            </div>\n        </div>\n    `,overlay.classList.add("show")}function showLoadingWithRetry(matchId){hideErrorOverlay(),hideNotLiveOverlay(),showLoadingMessage(),globalRetryManager.scheduleRetry(()=>{retryPlaybackSilently(matchId)})}async function retryPlaybackSilently(matchId){try{if(!await checkLiveStatusFromRedis(matchId))return globalRetryManager.reset(),void showNotLiveMessage();const container=document.getElementById("livePlayerContainer"),streamingType=container?.getAttribute("data-streaming-type")||document.querySelector('meta[name="match-streaming-type"]')?.content||"WebRTC",identifier=container?.getAttribute("data-identifier");"Cloudflare-Stream"!==streamingType&&"AWS-IVS"!==streamingType&&"MUX"!==streamingType||!identifier?startDetailPlayback(matchId,streamingType):await attemptDetailPlaybackOnce(matchId,streamingType,identifier)}catch(error){console.error("[RetryManager] 重试播放失败:",error)}}function hideNotLiveOverlay(){hideDetailErrorOverlay()}function hideErrorOverlay(){hideDetailErrorOverlay()}function showErrorOverlay(message,showRetry=!1){if(window.isSwitchingStreamer)return;if("function"==typeof isDetailPlayerActuallyPlaying&&isDetailPlayerActuallyPlaying())return void hideDetailErrorOverlay();if(message&&(String(message).includes("409")||String(message).includes("player.notLiveYet")))return hideLoadingMessage(),void showNotLiveMessage();const matchId=document.querySelector('meta[name="match-id"]')?.content||lastMatchId;
1matchId?handlePlaybackError(message,matchId):showNotLiveMessage()}function showAutoHideErrorTip(message,duration=3e3,type="error"){const existingTip=document.querySelector(".auto-hide-error-tip");let bgColor,icon;switch(existingTip&&existingTip.remove(),type){case"warning":bgColor="linear-gradient(135deg, #f39c12 0%, #e67e22 100%)",icon="fas fa-exclamation-triangle";break;case"info":bgColor="linear-gradient(135deg, #3498db 0%, #2980b9 100%)",icon="fas fa-info-circle";break;default:bgColor="linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)",icon="fas fa-exclamation-circle"}const errorTipHtml=`\n        <div class="auto-hide-error-tip" style="position: fixed; top: 20px; right: 20px; z-index: 10000; max-width: 400px;">\n            <div style="background: ${bgColor}; color: white; padding: 12px 16px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); display: flex; align-items: center; animation: slideInRight 0.3s ease-out;">\n                <i class="${icon}" style="margin-right: 10px; font-size: 16px;"></i>\n                <span style="flex: 1; font-size: 14px; line-height: 1.4;">${message}</span>\n                <button class="tip-close-btn" onclick="this.closest('.auto-hide-error-tip').remove()" style="margin-left: 10px; background: none; border: none; color: white; cursor: pointer; font-size: 18px; opacity: 0.8; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;">&times;</button>\n            </div>\n        </div>\n    `;if(!document.querySelector("#auto-hide-tip-styles")){const style=document.createElement("style");style.id="auto-hide-tip-styles",style.textContent="\n            @keyframes slideInRight {\n                from { transform: translateX(100%); opacity: 0; }\n                to { transform: translateX(0); opacity: 1; }\n            }\n            @keyframes slideOutRight {\n                from { transform: translateX(0); opacity: 1; }\n                to { transform: translateX(100%); opacity: 0; }\n            }\n            .auto-hide-error-tip.fade-out {\n                animation: slideOutRight 0.3s ease-in forwards;\n            }\n        ",document.head.appendChild(style)}document.body.insertAdjacentHTML("beforeend",errorTipHtml);const errorTip=document.querySelector(".auto-hide-error-tip");setTimeout(()=>{errorTip&&errorTip.parentNode&&(errorTip.classList.add("fade-out"),setTimeout(()=>{errorTip.parentNode&&errorTip.remove()},300))},duration),errorTip&&errorTip.addEventListener("click",e=>{e.target.classList.contains("tip-close-btn")||(errorTip.classList.add("fade-out"),setTimeout(()=>{errorTip.parentNode&&errorTip.remove()},300))})}function getLocalizedText(key,fallback){return window.L&&"function"==typeof window.L.T&&window.L.T(key)||fallback}function hideErrorOverlay(){hideDetailErrorOverlay(),void 0!==globalRetryManager&&globalRetryManager.reset&&globalRetryManager.reset()}function retryPlayback(){hideErrorOverlay(),playerRetryCount=0;const streamingType=document.querySelector('meta[name="match-streaming-type"]')?.content||"WebRTC",matchId=document.querySelector('meta[name="match-id"]')?.content||lastMatchId;matchId?(setDetailUserInitiatedPlay(),startDetailPlayback(matchId,streamingType)):console.error("[RetryPlayback] 无法获取比赛ID")}function switchStreamSource(sourceData,btn){if(document.querySelectorAll(".source-btn").forEach(b=>b.classList.remove("active")),btn&&btn.classList.add("active"),"WEBRTC"===(btn?.dataset?.streamingType||getStreamingType()).toUpperCase())loadWebRtcStream(sourceData);else{const matchIdMatch=sourceData.match(/\/hls\/(\d+)\//);loadHlsStream(sourceData,matchIdMatch?matchIdMatch[1]:null)}}function getStreamingType(){const streamingTypeMeta=document.querySelector('meta[name="match-streaming-type"]');return(streamingTypeMeta?.content||"HLS").toUpperCase()}async function loadWebRtcStream(matchId,skipStatusCheck=!1){const video=document.getElementById("livePlayer");if(!video)return;if(livekitRoom&&webrtcConnected&&!video.paused&&video.readyState>=2)return void(detailIsStreamPlaying=!0);
1const container=video.closest(".live-player-container"),setLoading=isLoading=>{container&&(isLoading?container.classList.add("loading"):container.classList.remove("loading"))};if(setLoading(!0),hideErrorOverlay(),!skipStatusCheck){const startResult=await startStream(matchId),isWebRtc="WEBRTC"===String(startResult.streamType||"").toUpperCase(),publishers=Number(startResult.publishers??0),status=startResult.status;if(!startResult.success){const errorMsg=startResult.message?translateServerMessage(startResult.message):window.L?.T?window.L.T("match.player.streamStartFailed"):"无法启动直播";return console.error("[DetailPlayer] WebRTC启动失败:",errorMsg),showErrorOverlay(errorMsg,!0),void setLoading(!1)}if(!isWebRtc||!(publishers>0||"ready"===status)){if(isWebRtc&&"starting"===status){setLoading(!1),showStreamStartingOverlay();let pollCount=0;const maxPolls=13,pollInterval=setInterval(async()=>{pollCount++;try{const checkResult=await checkStreamStatus(matchId);if(Number(checkResult.publishers??0)>0)return clearInterval(pollInterval),hideStreamStartingOverlay(),void loadWebRtcStream(matchId,!0)}catch(e){console.error("[DetailPlayer] 播放尝试异常:",e)}if(pollCount>=maxPolls){clearInterval(pollInterval),hideStreamStartingOverlay();try{const finalCheck=await checkStreamStatus(matchId);Number(finalCheck.publishers??0)>0?loadWebRtcStream(matchId,!0):showErrorOverlay(window.L?.T?window.L.T("match.player.streamTimeout"):"直播启动超时,请稍后重试",!0)}catch(e){console.error("[DetailPlayer] 最终状态检查异常:",e),showErrorOverlay(window.L?.T?window.L.T("match.player.streamTimeout"):"直播启动超时,请稍后重试",!0)}}},2e3);return}{const errorMsg=startResult.message?translateServerMessage(startResult.message):window.L?.T?window.L.T("match.player.streamError"):"直播源异常";return console.error("[DetailPlayer] WebRTC启动状态异常:",status),showErrorOverlay(errorMsg,!0),void setLoading(!1)}}setLoadTimeout()}if(hlsInstance&&(hlsInstance.destroy(),hlsInstance=null),livekitRoom){try{await livekitRoom.disconnect()}catch(e){console.warn("断开旧连接失败:",e)}livekitRoom=null}video.src="",video.load();try{window.LivekitClient||window.LiveKitClient||window.livekit||await loadLiveKitSDK();const tokenResponse=await fetch(`/api/webrtc/token/${matchId}`);if(!tokenResponse.ok){let errorDetails=`HTTP ${tokenResponse.status}`;try{errorDetails+=`: ${(await tokenResponse.json()).message||"未知错误"}`}catch(e){errorDetails+=": 服务器错误"}throw new Error(`获取token失败: ${errorDetails}`)}const tokenData=await tokenResponse.json();if(!tokenData.success||!tokenData.token)throw new Error(tokenData.message||"获取token失败");const{token:token,wsUrl:wsUrl}=tokenData;await connectToLiveKitRoom(token,wsUrl,video),setLoading(!1),hideErrorOverlay()}catch(error){setLoading(!1),console.error("[DetailPlayer] WebRTC播放异常:",error);const errorMessage=error.message||"";(errorMessage.includes("未找到WebRTC流")||errorMessage.includes("流未启动")||errorMessage.includes("not found")||errorMessage.includes("not streaming"))&&showErrorOverlay(L.T("match.player.streamNotStarted"),!1)}}function loadLiveKitSDK(){return new Promise((resolve,reject)=>{if(window.LivekitClient||window.LiveKitClient||window.livekit)return void resolve();const script=document.createElement("script");
1script.src="https://cdn.jsdelivr.net/npm/[email protected]/dist/livekit-client.umd.js",script.onload=()=>{resolve()},script.onerror=()=>{reject(new Error("LiveKit客户端加载失败"))},document.head.appendChild(script)})}async function connectToLiveKitRoom(token,wsUrl,video){const LiveKit=window.LivekitClient||window.LiveKitClient||window.livekit;if(!LiveKit)throw new Error("LiveKit客户端未加载");const{Room:Room}=LiveKit;try{window.audioContext||(window.audioContext=new(window.AudioContext||window.webkitAudioContext),"suspended"===window.audioContext.state&&window.audioContext.resume().catch(e=>{console.warn("AudioContext需要用户交互")}))}catch(audioError){console.warn("AudioContext初始化失败(忽略)")}const room=new Room({adaptiveStream:!0,dynacast:!0});livekitRoom=room,room.on("connected",()=>{webrtcConnected=!0}),room.on("disconnected",()=>{webrtcConnected=!1}),room.on("trackSubscribed",(track,publication,participant)=>{handleTrack(track,video)}),room.on("trackUnsubscribed",track=>{track.detach()}),room.on("participantConnected",participant=>{handleParticipant(participant,video)}),await room.connect(wsUrl,token),room.remoteParticipants.forEac
1h(participant=>{handleParticipant(participant,video)})}function handleParticipant(participant,video){participant.trackPublications.forEach(publication=>{publication.isSubscribed&&publication.track&&handleTrack(publication.track,video)}),participant.on("trackSubscribed",track=>{handleTrack(track,video)})}window.retryDetailPlayback=function(){hideDetailErrorOverlay();const matchId=document.querySelector('meta[name="match-id"]')?.content||lastMatchId,streamingType=document.querySelector('meta[name="match-streaming-type"]')?.content||lastStreamingType||"WebRTC";matchId?(showLoadingMessage(),startDetailPlayback(matchId,streamingType)):console.error("[DetailPlayer] 无法获取比赛ID")},window.resyncDetailLiveStream=async function resyncDetailLiveStream(){const matchId=document.querySelector('meta[name="match-id"]')?.content||lastMatchId,video=document.getElementById("livePlayer");if(!matchId||!video)return!1;const streamingType=(document.querySelector('meta[name="match-streaming-type"]')?.content||lastStreamingType||"WebRTC").toUpperCase().replace("-","_");if("WEBRTC"===streamingType||"LIVEKIT"===streamingType){if(livekitRoom){try{await livekitRoom.disconnect()}catch(e){console.warn("[DetailPlayer] LIVE 同步断开旧连接失败:",e)}livekitRoom=null,webrtcConnected=!1}return await loadWebRtcStream(matchId,!0),!0}if(window.ivsPlayerAdapter?.seekToLive?.())return window.ivsPlayerAdapter.play?.(),!0;const hls="function"==typeof window.getDetailHlsInstance?window.getDetailHlsInstance():null;if(hls&&Number.isFinite(hls.liveSyncPosition)&&hls.liveSyncPosition>0)return video.currentTime=hls.liveSyncPosition,await video.play().catch(()=>{}),!0;if(video.seekable&&video.seekable.length>0){const liveEdge=video.seekable.end(video.seekable.length-1);return video.currentTime=Math.max(0,liveEdge-.5),await video.play().catch(()=>{}),!0}return!1};let lastVideoTrackAttachTime=0;function handleTrack(track,video){const now=Date.now();if("video"===track.kind){if(track.attach(video),now-lastVideoTrackAttachTime<500)return;lastVideoTrackAttachTime=now,detailIsStreamPlaying=!0,isPlayingInProgress||(isPlayingInProgress=!0,video.muted||(video.muted=!0),video.play().then(()=>{isPlayingInProgress=!1,hideErrorOverlay(),hideLoadingMessage()}).catch(e=>{isPlayingInProgress=!1,"NotAllowedError"!==e.name&&"AbortError"!==e.name&&console.warn("⚠️ 视频播放异常:",e.message)}))}else"audio"===track.kind&&track.attach(video)}function hidePlayButtonOverlay(video){const container=video?.parentElement;if(!container)return;const overlays=container.querySelectorAll(".play-button-overlay");overlays.length>0&&overlays.forEach(overlay=>{overlay.remove()})}function addPlayButtonOverlay(video){const container=video.parentElement;if(!container)return void console.error("❌ No container found for video overlay");if(container.querySelector(".play-button-overlay"))return;const overlay=document.createElement("div");overlay.className="play-button-overlay",overlay.style.cssText="\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        background: rgba(0,0,0,0.7);\n        color: white;\n        border: none;\n        border-radius: 50%;\n        width: 60px;\n        height: 60px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        z-index: 9999;\n        font-size: 24px;\n        pointer-events: auto;\n    ",overlay.innerHTML="▶️",overlay.addEventListener("click",()=>{hidePlayButtonOverlay(video),video.play().then(()=>{}).catch(e=>{console.error("❌ 用户手动播放失败:",e.message)})}),"static"===getComputedStyle(container).position&&(container.style.position="relative"),container.appendChild(overlay),container.children.length,setTimeout(()=>{const verifyOverlay=container.querySelector(".play-button-overlay");verifyOverlay&&getComputedStyle(verifyOverlay)},100)}function initializeWebRTCPlayer(url,video){console.warn("initializeWebRTCPlayer已废弃,请使用connectToLiveKitRoom")}function addWebRTCControls(player){const video=document.getElementById("livePlayer");if(!video)return;const controls=document.createElement("div");controls.className="position-absolute bottom-0 start-0 end-0 p-3",controls.style.zIndex="100",controls.style.background="linear-gradient(transparent, rgba(0,0,0,0.7))",controls.innerHTML=`\n        <div class="d-flex justify-content-between align-items-center">\n            <div class="text-white">\n                <i class="bi bi-broadcast"></i>
1 WebRTC 直播\n                <span id="webrtc-status" class="badge bg-warning ms-2">连接中...</span>\n            </div>\n            <div class="text-white">\n                <button class="btn btn-sm btn-outline-light me-2" onclick="toggleWebRTCStats()">\n                    <i class="bi bi-speedometer2"></i> 统计\n                </button>\n                <button class="btn btn-sm btn-outline-danger" onclick="stopWebRTCStream()">\n                    <i class="bi bi-stop-circle"></i> 停止\n                </button>\n            </div>\n        </div>\n        <div id="webrtc-stats" class="text-white small mt-2" style="display: none;">\n            <div>连接状态: <span id="connection-status">检查中...</span></div>\n            <div>服务器: ws://localhost:3000</div>\n            <div>房间: match-${document.querySelector('meta[name="match-id"]')?.content||"unknown"}</div>\n        </div>\n    `;const existingControls=video.parentElement.querySelector(".position-absolute.bottom-0");existingControls&&existingControls.remove(),video.parentElement.appendChild(controls);const statusInterval=setInterval(()=>{const status=player.mediasoupClient.getConnectionState(),statusElement=document.getElementById("connection-status"),badgeElement=document.getElementById("webrtc-status");status.connected?(statusElement&&(statusElement.textContent="已连接"),badgeElement&&(badgeElement.textContent="已连接",badgeElement.className="badge bg-success ms-2")):(statusElement&&(statusElement.textContent="未连接"),badgeElement&&(badgeElement.textContent="未连接",badgeElement.className="badge bg-danger ms-2"))},2e3);player.statusInterval=statusInterval}function toggleWebRTCStats(){const stats=document.getElementById("webrtc-stats");stats&&(stats.style.display="none"===stats.style.display?"block":"none")}function stopWebRTCStream(){if(window.currentWebRTCPlayer){window.currentWebRTCPlayer.stop(),window.currentWebRTCPlayer.statusInterval&&clearInterval(window.currentWebRTCPlayer.statusInterval);const controls=document.querySelector(".position-absolute.bottom-0");controls&&controls.remove(),window.currentWebRTCPlayer=null}}function autoOpenLivePlayer(){const playerSection=document.getElementById("livePlayerSection");playerSection&&(initDetailVisibilityHandler(),playerSection.classList.contains("collapsed")?setTimeout(()=>toggleLivePlayer(),100):setTimeout(()=>{const hasVideo="true"===document.querySelector('meta[name="match-has-video"]')?.content,hasStreamSources="true"===document.querySelector('meta[name="match-has-stream-sources"]')?.content,hasStreamerTask="true"===document.querySelector('meta[name="match-has-streamer-task"]')?.content,streamingType=document.querySelector('meta[name="match-streaming-type"]')?.content||"null";if(!(hasVideo||hasStreamSources||hasStreamerTask))return void showErrorOverlay(window.L?window.L.T("match.noLiveStream"):"暂无直播源",!1);const matchIdMeta=document.querySelector('meta[name="match-id"]');if(matchIdMeta){const matchId=matchIdMeta.content,selectedStreamer=getSmartStreamerSelection(matchId);if(selectedStreamer){const container=document.getElementById("livePlayerContainer");container&&(container.setAttribute("data-stream-session-id",selectedStreamer.streamSessionId),container.setAttribute("data-streaming-type",selectedStreamer.templateType),container.setAttribute("data-identifier",`${matchId}_${selectedStreamer.streamSessionId}`)),updateStreamerButtonState(selectedStreamer.streamSessionId),hideLoadingMessage(),setDetailUserInitiatedPlay(),startDetailPlayback(matchId,selectedStreamer.templateType)}else hideLoadingMessage(),setDetailUserInitiatedPlay(),startDetailPlayback(matchId,streamingType)}},300))}function getSmartStreamerSelection(matchId){try{const streamerButtons=document.querySelectorAll(".streamer-btn");if(0===streamerButtons.length)return null;const streamers=Array.from(streamerButtons).map(btn=>({streamSessionId:btn.getAttribute("data-stream-session-id"),templateType:btn.getAttribute("data-template-type"),slug:btn.getAttribute("data-streamer-slug"),isLive:null!==btn.querySelector(".live-indicator"),element:btn})),urlSlug=window.selectedStreamerSlug;if(urlSlug){const fromUrl=streamers.find(s=>s.slug===urlSlug);
1if(fromUrl)return{streamSessionId:fromUrl.streamSessionId,templateType:fromUrl.templateType}}if(streamers.length,streamers.map(s=>({id:s.streamSessionId,type:s.templateType,live:s.isLive})),void 0!==window.streamerCacheManager){const cachedSelection=window.streamerCacheManager.getStreamerSelection(matchId);if(cachedSelection){if(streamers.find(s=>s.streamSessionId==cachedSelection.streamSessionId))return cachedSelection;cachedSelection.streamSessionId,streamers.map(s=>s.streamSessionId),window.streamerCacheManager.clearStreamerSelection(matchId)}}else console.warn("[SmartStreamer] 缓存管理器未加载");const liveStreamer=streamers.find(s=>s.isLive);return liveStreamer?(liveStreamer.streamSessionId,{streamSessionId:liveStreamer.streamSessionId,templateType:liveStreamer.templateType}):streamers.length>0?(streamers[0].streamSessionId,{streamSessionId:streamers[0].streamSessionId,templateType:streamers[0].templateType}):null}catch(error){return console.error("[SmartStreamer] 选择主播失败:",error),null}}function updateStreamerButtonState(activeStreamSessionId){try{document.querySelectorAll(".streamer-btn").forEach(btn=>{btn.getAttribute("data-stream-session-id")===activeStreamSessionId?btn.classList.add("active"):btn.classList.remove("active")})}catch(error){console.error("[SmartStreamer] 更新按钮状态失败:",error)}}let visibilityHandlerInitialized=!1,detailPersistentPlaybackManager=null;function getDetailStreamingType(){return lastStreamingType||document.querySelector('meta[name="match-streaming-type"]')?.content||null}function isDetailPlayerActuallyPlaying(){const video=document.getElementById("livePlayer");return!!video&&(!!detailIsStreamPlaying||!video.paused&&video.readyState>=2&&video.videoWidth>0||!(!ivsPlayerAdapter||!ivsPlayerAdapter.getState||"playing"!==ivsPlayerAdapter.getState())||!(!livekitRoom||"connected"!==livekitRoom.state||video.paused))}function isDetailWatchingOrWaiting(){return!!(isDetailPlayerActuallyPlaying()||isStreamStartingWait||publisherPollingTimer||detailStreamPollingTimer||hasDetailUserInitiatedPlay())}function pcShortHideDetail(){stopPublisherPolling(),stopDetailStreamPolling(),detailPersistentPlaybackManager&&detailPersistentPlaybackManager.notifyPageHidden()}async function resumeDetailAfterShortHide(resumeDetailPlayerFn){if(detailPersistentPlaybackManager&&isDetailStreamStillConnected()&&await detailPersistentPlaybackManager.resumeAfterShortHide(()=>ivsPlayerAdapter?.getState?.()))return detailIsStreamPlaying=!0,void hideDetailPlayOverlay();await softResumeDetailPlayer(resumeDetailPlayerFn);const streamingType=getDetailStreamingType();if(!lastMatchId||!streamingType)return;const type=streamingType.toUpperCase();"WEBRTC"===type||"LIVEKIT"===type?startPublisherPolling(lastMatchId):null===detailStreamPollingTimer&&hasDetailUserInitiatedPlay()&&startDetailStreamPolling(lastMatchId,streamingType)}function pcLongHideTeardownDetail(){stopPublisherPolling(),stopDetailStreamPolling(),cleanupAllPlayers();const video=document.getElementById("livePlayer");if(video)try{video.pause(),video.removeAttribute("src"),video.src="",video.srcObject=null,video.load()}catch(e){console.warn("[DetailPlayer] 长隐藏 teardown 重置 video 失败:",e)}}async function resumeDetailAfterTeardown(){if(await new Promise(r=>setTimeout(r,500)),!lastMatchId)return;const streamingType=getDetailStreamingType()||lastStreamingType;if(!streamingType)return;setDetailUserInitiatedPlay(),hideDetailPlayOverlay(),hideDetailErrorOverlay(),showLoadingMessage();const video=document.getElementById("livePlayer");video&&!video.muted&&(video.muted=!0);const container=document.getElementById("livePlayerContainer"),identifier=container?.getAttribute("data-identifier")||null;await loadStreamWithRetry(lastMatchId,streamingType,identifier)}function isDetailStreamStillConnected(){const video=document.getElementById("livePlayer");return!(!ivsPlayerAdapter||!ivsPlayerAdapter.getState||"destroyed"===ivsPlayerAdapter.getState())||!(!livekitRoom||"connected"!==livekitRoom.state)||!!(video&&(video.srcObject||video.src)&&video.readyState>=1)}async function softResumeDetailPlayer(resumeDetailPlayerFn){const video=document.getElementById("livePlayer");if(!video)return;const streamType=(getDetailStreamingType()||"").toUpperCase().replace(/_/g,"-");ivsPlayerAdapter&&("AWS-IVS"===streamType||"IVS"===streamType||"AWSIVS"===streamType)&&"function"==typeof ivsPlayerAdapter.play&&ivsPlayerAdapter.play(),"function"!=typeof resumeDetailPlayerFn?video.paused&&safePlay(video):await resumeDetailPlayerFn(v
1ideo)}function initPcDetailPlaybackVisibility(resumeDetailPlayerFn){void 0!==window.PcPlaybackVisibility&&(window.PcPlaybackVisibility.registerDetailHandlers({getStreamingType:getDetailStreamingType,isWatchingOrWaiting:isDetailWatchingOrWaiting,isStreamStillConnected:isDetailStreamStillConnected,onShortHidePause:pcShortHideDetail,onLongHideTeardown:pcLongHideTeardownDetail,onResumeAfterShortHide:()=>resumeDetailAfterShortHide(resumeDetailPlayerFn),onResumeAfterTeardown:resumeDetailAfterTeardown,onResumePendingAutoPlay:async()=>{if(isDetailStreamStillConnected())return void await resumeDetailAfterShortHide(resumeDetailPlayerFn);if(!lastMatchId||!getDetailStreamingType())return;const streamingType=getDetailStreamingType(),type=streamingType.toUpperCase();"WEBRTC"!==type&&"LIVEKIT"!==type?reloadCurrentStream():await tryLoadDetailStreamSilent(lastMatchId,streamingType)?(detailIsStreamPlaying=!0,stopDetailStreamPolling()):publisherPollingTimer||startPublisherPolling(lastMatchId)}}),window.PcPlaybackVisibility.init())}function initDetailVisibilityHandler(){if(visibilityHandlerInitialized)return;visibilityHandlerInitialized=!0;const video=document.getElementById("livePlayer");if(video){if("undefined"!=typeof PersistentPlaybackManager){const isDetailSourceActive=()=>!(!ivsPlayerAdapter||"destroyed"===ivsPlayerAdapter.getState())||!(!video.srcObject&&!video.src),resumeDetailPlayer=async targetVideo=>{const streamType=(lastStreamingType||"").toUpperCase().replace(/_/g,"-");if(ivsPlayerAdapter&&("AWS-IVS"===streamType||"IVS"===streamType||"AWSIVS"===streamType)){const state=ivsPlayerAdapter.getState();if("buffering"===state){for(let i=0;i<15;i++)if(await new Promise(r=>setTimeout(r,200)),!targetVideo.paused&&targetVideo.readyState>=2)return!0;return!targetVideo.paused}if("ready"===state&&(await new Promise(r=>setTimeout(r,150)),!targetVideo.paused&&!targetVideo.ended))return!0;ivsPlayerAdapter.play();for(let i=0;i<12;i++)if(await new Promise(r=>setTimeout(r,200)),!targetVideo.paused&&targetVideo.readyState>=2)return!0;return!targetVideo.paused}try{return await targetVideo.play(),!0}catch(error){if("NotAllowedError"===error.name)return targetVideo.muted=!0,await targetVideo.play(),!0;throw error}};detailPersistentPlaybackManager=new PersistentPlaybackManager(video,{playerId:"livePlayer",enableAutoPiP:!0,enableAutoResume:!0,enableMobileAutoPiP:!0,resumeDelay:500,showLoadingOnResume:!1,skipVisibilityHandling:void 0!==window.PcPlaybackVisibility&&window.PcPlaybackVisibility.isActive(),reconnectAttempts:2,reconnectDelay:800,isSourceActive:isDetailSourceActive,resumePlayer:resumeDetailPlayer,pausePlayer:async()=>{try{if(ivsPlayerAdapter&&"function"==typeof ivsPlayerAdapter.pause&&ivsPlayerAdapter.getState&&"destroyed"!==ivsPlayerAdapter.getState())return void ivsPlayerAdapter.pause()}catch(e){}video&&"function"==typeof video.pause&&video.pause()},getStreamInfo:()=>({streamType:lastStreamingType||"WebRTC",matchId:lastMatchId}),reconnectStream:async(streamType,matchId)=>(detailIsStreamPlaying=!1,setDetailUserInitiatedPlay(),"function"==typeof startDetailPlayback?startDetailPlayback(matchId,streamType):reloadCurrentStream(),!0),onShowLoading:()=>{"function"==typeof showLoadingMessage&&showLoadingMessage()},onHideLoading:()=>{"function"==typeof hideLoadingMessage&&hideLoadingMessage()},onError:error=>{console.error("[DetailPlayer] PersistentPlayback 错误:",error.message),"function"==typeof showErrorOverlay&&showErrorOverlay(error.message,!0)}}),detailPersistentPlaybackManager.initialize(),window.detailPersistentPlaybackManager=detailPersistentPlaybackManager,window.detailPiPManager=detailPersistentPlaybackManager.pipManager||null,initPcDetailPlaybackVisibility(resumeDetailPlayer)}else void 0!==window.PcPlaybackVisibility&&window.PcPlaybackVisibility.isActive()?initPcDetailPlaybackVisibility(null):initSimpleDetailVisibilityHandler();window.addEventListener("beforeunload",function(){cleanupAllPlayers()})}}function initSimpleDetailVisibilityHandler(){let mobileHidePauseTimer=null,wasPlayingBeforeHidden=!1;document.addEventListener("visibilitychange",function(){const video=document.getElementById("livePlayer");if(video)if(document.hidden)wasPlayingBeforeHidden=!video.paused&&!video.ended&&detailIsStreamPlaying,mobileHidePauseTimer&&clearTimeout(mobileHidePauseTimer),(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1)&&wasPlayingBeforeHidden&&(mobileHidePauseTimer=setTimeout(()=>
1{if(mobileHidePauseTimer=null,document.hidden)try{if(document.pictureInPictureElement===video)return;ivsPlayerAdapter&&"function"==typeof ivsPlayerAdapter.pause?ivsPlayerAdapter.pause():video.pause()}catch(e){}},2500));else{mobileHidePauseTimer&&(clearTimeout(mobileHidePauseTimer),mobileHidePauseTimer=null);const hasSource=video.srcObject||video.src||ivsPlayerAdapter&&ivsPlayerAdapter.getState&&"destroyed"!==ivsPlayerAdapter.getState(),isVideoBlack=!video.videoWidth||0===video.videoWidth;(wasPlayingBeforeHidden||detailIsStreamPlaying)&&(video.paused||isVideoBlack)&&(hasSource&&!isVideoBlack||ivsPlayerAdapter?video.paused&&(ivsPlayerAdapter&&"function"==typeof ivsPlayerAdapter.play?ivsPlayerAdapter.play():safePlay(video)):lastMatchId&&lastStreamingType&&setTimeout(()=>{document.hidden||reloadCurrentStream()},500)),wasPlayingBeforeHidden=!1}})}function cleanupAllPlayers(){if(ivsPlayerAdapter){try{ivsPlayerAdapter.destroy()}catch(e){console.warn("[DetailPlayer] 清理 IVS 播放器失败:",e.message)}ivsPlayerAdapter=null,window.ivsPlayerAdapter=null}if(hlsInstance){try{hlsInstance.destroy()}catch(e){console.warn("[DetailPlayer] 清理 HLS 实例失败:",e.message)}hlsInstance=null}if(livekitRoom){try{livekitRoom.disconnect()}catch(e){console.warn("[DetailPlayer] 清理 LiveKit 连接失败:",e.message)}livekitRoom=null}const video=document.getElementById("livePlayer");video&&(video.src="",video.srcObject=null),detailCfQualitySelectorInstance&&(detailCfQualitySelectorInstance=null);const cfQualitySelector=document.getElementById("detailCfQualitySelector");cfQualitySelector&&cfQualitySelector.remove(),detailIsStreamPlaying=!1,webrtcConnected=!1}function safePlay(video){video&&!isPlayingInProgress&&(isPlayingInProgress=!0,video.play().then(()=>{isPlayingInProgress=!1}).catch(e=>{e.message,isPlayingInProgress=!1}))}function reloadCurrentStream(){if(!lastMatchId)return;const video=document.getElementById("livePlayer");if((!video||video.paused||video.ended||!(video.readyState>2))&&!isStreamStartingWait)if(detailIsStreamPlaying=!1,setDetailUserInitiatedPlay(),startDetailPlayback(lastMatchId,lastStreamingType),"WEBRTC"===lastStreamingType?.toUpperCase())loadWebRtcStream(lastMatchId);else{const activeSource=document.querySelector(".source-btn.active"),sourceUrl=activeSource?.dataset?.sourceUrl;loadHlsStream(sourceUrl||`/hls/${lastMatchId}/index.m3u8`,lastMatchId)}}const DETAIL_MAX_RETRY_COUNT=3,DETAIL_RETRY_INTERVAL=3e3;let detailRetryCount=0,detailRetryTimer=null,detailNotInTimeWindow=!1;function clearDetailRetryTimer(){detailRetryTimer&&(clearTimeout(detailRetryTimer),detailRetryTimer=null)}async function loadStreamWithRetry(matchId,streamingType,identifier=null){detailIsStreamPlaying=!1,lastMatchId=matchId,lastStreamingType=streamingType,detailRetryCount=0,detailNotInTimeWindow=!1,clearDetailRetryTimer(),await attemptDetailPlaybackOnce(matchId,streamingType,identifier)}async function attemptDetailPlaybackOnce(matchId,streamingType,identifier=null){if(!matchId)return console.error("[DetailPlayer] matchId为空,无法播放"),void showDetailPlayerError("比赛ID无效",!0);if(await tryLoadDetailStream(matchId,streamingType,identifier))detailIsStreamPlaying=!0,detailRetryCount=0,detailNotInTimeWindow=!1,clearDetailRetryTimer(),stopDetailStreamPolling(),clearLoadTimeout(),hideErrorOverlay();else{if(window.isSwitchingStreamer)return;showDetailPlayerError(window.L?.T?window.L.T("match.player.streamTimeout"):"直播连接失败,请重试",!0)}}async function attemptDetailPlayback(matchId,streamingType){return await attemptDetailPlaybackOnce(matchId,streamingType)}async function tryLoadDetailStream(matchId,streamingType,identifier=null){try{const type=streamingType?.toUpperCase()?.replace("-","_")||"HLS";return"WEBRTC"===type?await tryLoadDetailWebRtcStream(matchId):"AWS_IVS"===type||"AWSIVS"===type||"IVS"===type?await tryLoadDetailIvsStream(matchId,identifier):"MUX"===type?await tryLoadDetailMuxStream(matchId,identifier):"CLOUDFLARE_STREAM"===type||"CLOUDFLARESTREAM"===type||"CLOUDFLARE"===type||"CF"===type?await tryLoadDetailCloudflareStream(matchId,identifier):await tr
1yLoadDetailHlsStream(matchId)}catch(e){return e.message,!1}}async function tryLoadDetailStreamSilent(matchId,streamingType){try{const type=streamingType?.toUpperCase()?.replace("-","_")||"HLS";return"WEBRTC"===type?await tryLoadDetailWebRtcStreamSilent(matchId):"AWS_IVS"===type||"AWSIVS"===type||"IVS"===type?await tryLoadDetailIvsStreamSilent(matchId):"MUX"===type?await tryLoadDetailMuxStreamSilent(matchId,identifier):"CLOUDFLARE_STREAM"===type||"CLOUDFLARESTREAM"===type||"CLOUDFLARE"===type||"CF"===type?await tryLoadDetailCloudflareStreamSilent(matchId):await tryLoadDetailHlsStreamSilent(matchId)}catch(e){return e.message,!1}}async function tryLoadDetailHlsStreamSilent(matchId){const activeSource=document.querySelector(".source-btn.active"),sourceUrl=activeSource?.dataset?.sourceUrl,hlsUrl=sourceUrl||`/hls/${matchId}/index.m3u8`;try{return!!(await fetch(hlsUrl,{method:"HEAD"})).ok&&(loadHlsStream(hlsUrl,matchId),new Promise(resolve=>{const timeout=setTimeout(()=>resolve(!1),5e3),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)}))}catch(e){return!1}}async function tryLoadDetailWebRtcStreamSilent(matchId){try{if(detailIsStreamPlaying)return!0;if(livekitRoom&&webrtcConnected){const video=document.getElementById("livePlayer");if(video&&!video.paused&&video.readyState>=2)return detailIsStreamPlaying=!0,!0}const statusResult=await checkStreamStatus(matchId),isWebRtc="WEBRTC"===String(statusResult?.streamType||"").toUpperCase(),publishers=Number(statusResult?.publishers??0);if(isWebRtc&&0===publishers)return!1;const tokenResponse=await fetch(`/api/webrtc/token/${matchId}`);if(!tokenResponse.ok)return!1;const tokenData=await tokenResponse.json();return!(!tokenData.success||!tokenData.token)&&(loadWebRtcStream(matchId),new Promise(resolve=>{const timeout=setTimeout(()=>resolve(!1),1e4),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)}))}catch(e){return!1}}async function getMuxStreamInfo(identifier){try{const response=await fetch(`/api/stream/info/${identifier}`);return response.ok?await response.json():null}catch(e){return console.error("[DetailPlayer] 获取 Mux 流信息异常:",e.message),null}}async function getMuxPlaybackUrl(identifier){const data=await getMuxStreamInfo(identifier);return data?.playbackUrl||data?.playUrl||null}async function tryLoadDetailMuxStream(matchId,identifier){try{const effectiveIdentifier=identifier||matchId,streamInfo=await getMuxStreamInfo(effectiveIdentifier),playbackUrl=streamInfo?.playbackUrl||streamInfo?.playUrl;return playbackUrl?(await loadHlsStream(playbackUrl,null,{enableQualitySelector:!0,skipStartStream:!0,hlsVariants:streamInfo?.variants}),new Promise(resolve=>{const timeout=setTimeout(()=>resolve(!1),1e4),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)})):(console.error("[DetailPlayer] 无法获取 Mux 播放地址"),!1)}catch(e){return console.error("[DetailPlayer] Mux 加载异常:",e.message),!1}}async function tryLoadDetailMuxStreamSilent(matchId,identifier=null){try{const effectiveIdentifier=identifier||matchId,streamInfo=await getMuxStreamInfo(effectiveIdentifier),playbackUrl=streamInfo?.playbackUrl||streamInfo?.playUrl;return!!playbackUrl&&(loadHlsStream(playbackUrl,null,{enableQualitySelector:!0,skipStartStream:!0,hlsVariants:streamInfo?.variants}),new Promise(resolve=>{const timeout=setTimeout(()=>resolve(!1),5e3),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)}))}catch(e){return!1}}let ivsPlayerAdapter=null;async function tryLoadDetailIvsStream(matchId,identifier){try{if("undefined"==typeof IVSPlayerAdapter)return console.error("[DetailPlayer] IVS 播放器适配器未加载"),!1;const effectiveIdentifier=identifier||matchId,playbackUrl=await getIvsPlaybackUrl(effectiveIdentifier);return playbackUrl?(await loadIvsStream(playbackUrl,matchId),new Promise(resolve=>{const timeout=setTimeout(()=>{resolve(!1)},1e4),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)})):(console.error("[DetailPlayer] 无法获取 IVS 播放地址"),
1!1)}catch(e){return console.error("[DetailPlayer] IVS 加载异常:",e.message),!1}}async function tryLoadDetailIvsStreamSilent(matchId){try{if(detailIsStreamPlaying)return!0;if("undefined"==typeof IVSPlayerAdapter)return await tryLoadDetailHlsStreamSilent(matchId);const playbackUrl=await getIvsPlaybackUrl(matchId);return playbackUrl?(await loadIvsStream(playbackUrl,matchId),new Promise(resolve=>{const timeout=setTimeout(()=>resolve(!1),1e4),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)})):await tryLoadDetailHlsStreamSilent(matchId)}catch(e){return await tryLoadDetailHlsStreamSilent(matchId)}}async function tryLoadDetailCloudflareStream(matchId,identifier){try{if("undefined"==typeof initCloudflareWhepPlayer)return console.error("[DetailPlayer] Cloudflare WHEP 播放器未加载"),!1;const effectiveIdentifier=identifier||matchId,whepUrl=await getCloudflareWhepUrl(effectiveIdentifier);if(!whepUrl)return console.error("[DetailPlayer] 无法获取 Cloudflare WHEP 播放地址"),!1;const video=document.getElementById("livePlayer");return video?(await initCloudflareWhepPlayer(video,whepUrl),new Promise(resolve=>{const timeout=setTimeout(()=>{resolve(!1)},15e3),onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying),video.play().catch(e=>{console.warn("[DetailPlayer] Cloudflare Stream 自动播放失败:",e.message)})})):(console.warn("[DetailPlayer] 视频元素不存在"),!1)}catch(e){return console.error("[DetailPlayer] Cloudflare Stream 加载异常:",e.message),!1}}async function tryLoadDetailCloudflareStreamSilent(matchId){try{if(detailIsStreamPlaying)return!0;if("function"==typeof isCloudflareWhepConnected&&isCloudflareWhepConnected()){const video=document.getElementById("livePlayer");if(video&&!video.paused&&video.readyState>=2)return detailIsStreamPlaying=!0,!0}if("undefined"==typeof initCloudflareWhepPlayer)return await tryLoadDetailHlsStreamSilent(matchId);const whepUrl=await getCloudflareWhepUrl(matchId);if(!whepUrl)return await tryLoadDetailHlsStreamSilent(matchId);const video=document.getElementById("livePlayer");return!!video&&(await initCloudflareWhepPlayer(video,whepUrl),new Promise(resolve=>{const timeout=setTimeout(()=>resolve(!1),15e3),onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying),video.play().catch(()=>{})}))}catch(e){return await tryLoadDetailHlsStreamSilent(matchId)}}function initHomeCloudflareWhepEventListeners(){window.addEventListener("cloudflareWhepConnected",event=>{event.detail.whepUrl,hideLoadingMessage(),hideErrorOverlay(),void 0!==window.isStreamPlaying&&(window.isStreamPlaying=!0)}),window.addEventListener("cloudflareWhepStalled",event=>{event.detail.reason,showLoadingMessage(),void 0!==window.isStreamPlaying&&(window.isStreamPlaying=!1)}),window.addEventListener("cloudflareWhepRecovered",event=>{hideLoadingMessage(),void 0!==window.isStreamPlaying&&(window.isStreamPlaying=!0)}),window.addEventListener("cloudflareWhepDisconnected",event=>{event.detail.reason;const container=document.getElementById("livePlayerContainer"),identifier=container?.dataset?.identifier;identifier?(void 0!==window.isStreamPlaying&&(window.isStreamPlaying=!1),showErrorOverlay(window.L?.T?window.L.T("match.player.streamDisconnected"):"直播连接断开",!0)):console.error("[HomePlayer] 无法获取identifier,无法重试")}),window.addEventListener("cloudflareWhepError",event=>{event.detail.error,void 0!==window.isStreamPlaying&&(window.isStreamPlaying=!1),showErrorOverlay(event.detail.error,!0)}),window.addEventListener("cloudflareWhepClosed",event=>{event.detail.whepUrl,void 0!==window.isStreamPlaying&&(window.isStreamPlaying=!1)})}async function retryHomeCloudflareWhepConnection(whepUrl){try{let video=document.getElementById("livePlayer");if(video||(video=document.getElementById("focusPlayer")),video||(video=document.querySelector("video")),!video)return console.error("[HomePlayer] ❌ 找不到任何视频元素,无法重连"),void setTimeout(()=>{retryHomeCloudflareWhepConnection(whepUrl)},5e3);video.id,"function"==typeof destroyCloudflareWhepPlayer&&(destroyCloudflareWhepPlayer(),await new Promise(resolve=>setTimeout(resolve,1e3))),await initCloudflareWhepPlayer(video,whepUrl)}catch(error){console.error("[HomePlayer] ❌ CF WHEP重连失败:",error),setTimeout(()=>{retryHomeCloudflareWhepConnection(whepUrl)},5e3)}}function initDetailCloudflareWhepEventListeners(){window.addEventListener("cloudflareWhepConnected",event=>
1{event.detail.whepUrl,hideLoadingMessage(),hideErrorOverlay(),detailIsStreamPlaying=!0}),window.addEventListener("cloudflareWhepStalled",event=>{event.detail.reason,detailIsStreamPlaying=!1,showLoadingMessage()}),window.addEventListener("cloudflareWhepRecovered",event=>{hideLoadingMessage(),detailIsStreamPlaying=!0}),window.addEventListener("cloudflareWhepDisconnected",event=>{event.detail.reason,detailIsStreamPlaying=!1,window.isSwitchingStreamer||showDetailPlayerError(window.L?.T?window.L.T("match.player.streamDisconnected"):"直播连接断开",!0)}),window.addEventListener("cloudflareWhepError",event=>{event.detail.error,detailIsStreamPlaying=!1;const error=event.detail.originalError||event.detail.error;if(error&&(error.isWaitingForStream||409===error.status||"string"==typeof error&&error.includes("409")))return hideLoadingMessage(),void showNotLiveMessage();window.isSwitchingStreamer||showDetailPlayerError(event.detail.error||"播放失败",!0)}),window.addEventListener("cloudflareWhepClosed",event=>{event.detail.whepUrl,detailIsStreamPlaying=!1})}async function retryDetailCloudflareWhepConnection(matchId,whepUrl){try{let video=document.getElementById("livePlayer");if(video||(video=document.getElementById("focusPlayer")),video||(video=document.querySelector("video")),!video)return console.error("[DetailPlayer] ❌ 找不到任何视频元素,无法重连"),void setTimeout(()=>{retryDetailCloudflareWhepConnection(matchId,whepUrl)},5e3);video.id,"function"==typeof destroyCloudflareWhepPlayer&&(destroyCloudflareWhepPlayer(),await new Promise(resolve=>setTimeout(resolve,1e3))),await initCloudflareWhepPlayer(video,whepUrl)}catch(error){console.error("[DetailPlayer] ❌ CF WHEP重连失败:",error),setTimeout(()=>{retryDetailCloudflareWhepConnection(matchId,whepUrl)},5e3)}}async function retryDetailCloudflareWhepConnectionDirect(whepUrl){try{const video=document.getElementById("livePlayer");if(!video)return void console.error("[DetailPlayer] ❌ 视频元素不存在,无法重连");await initCloudflareWhepPlayer(video,whepUrl)}catch(error){console.error("[DetailPlayer] ❌ CF WHEP直接重连失败:",error),setTimeout(()=>{retryDetailCloudflareWhepConnectionDirect(whepUrl)},5e3)}}function getCurrentDetailMatchId(){const pathMatch=window.location.pathname.match(/\/match\/(\d+)/);if(pathMatch)return pathMatch[1],pathMatch[1];const metaMatchId=document.querySelector('meta[name="match-id"]')?.content;if(metaMatchId)return metaMatchId;const matchElement=document.querySelector("[data-match-id]");if(matchElement){return matchElement.getAttribute("data-match-id")}const altPathMatch=window.location.pathname.match(/\/(\d+)$/);return altPathMatch?(altPathMatch[1],altPathMatch[1]):(console.error("[DetailPlayer] 无法从任何来源获取比赛ID"),window.location.pathname,document.querySelector('meta[name="match-id"]'),null)}window.ivsPlayerAdapter=null;let detailCfQualitySelectorInstance=null,detailCurrentCfQuality=null;async function getCloudflareWhepUrl(matchId){try{const response=await fetch(`/api/fixtures/cf-quality-options/${matchId}`);if(response.ok){const data=await response.json();if(data.success&&data.data&&data.data.length>0){const qualityOptions=data.data;showDetailCfQualitySelector(qualityOptions,matchId);const selectedQuality=localStorage.getItem("cf_selected_quality")||"1080p",quality=qualityOptions.find(q=>q.quality===selectedQuality)||qualityOptions[0];return quality.quality,quality.whepUrl,quality.whepUrl}}const metaPlayUrl=document.querySelector('meta[name="match-play-url"]')?.content;if(metaPlayUrl&&metaPlayUrl.includes("cloudflarestream.com"))return metaPlayUrl;const infoResponse=await fetch(`/api/stream/info/${matchId}`);if(!infoResponse.ok)return console.warn("[DetailPlayer] 获取流信息失败:",infoResponse.status),null;const data=await infoResponse.json();return data.playUrl||data.whepUrl||null}catch(e){return console.error("[DetailPlayer] 获取 Cloudflare WHEP 地址异常:",e.message),null}}function showDetailCfQualitySelector(options,matchId){if(options&&options.length,!options||0===options.length)return;if(void 0===window.QualitySelector)return void console.error("[DetailPlayer] QualitySelector 类未找到!");
1const existing=document.getElementById("detailCfQualitySelector");existing&&existing.remove();const selector=document.createElement("div");selector.id="detailCfQualitySelector",selector.className="cf-quality-selector-container";const actionsContainer=document.querySelector(".focus-player-actions"),playerContainer=document.querySelector(".live-player-container")||document.getElementById("livePlayer")?.parentElement;if(actionsContainer){const insertAnchor=actionsContainer.querySelector("#focusMatchDetailLink")||actionsContainer.querySelector(".player-more-dropdown")||actionsContainer.firstChild;actionsContainer.insertBefore(selector,insertAnchor)}else{if(!playerContainer)return void console.error("[DetailPlayer] 找不到合适的容器插入选择器!");"static"===getComputedStyle(playerContainer).position&&(playerContainer.style.position="relative"),playerContainer.appendChild(selector)}const qualities=options.map(opt=>({quality:opt.quality,label:opt.displayName,whepUrl:opt.whepUrl}));detailCurrentCfQuality=localStorage.getItem("cf_selected_quality")||qualities[0].quality,detailCfQualitySelectorInstance=new window.QualitySelector(selector,{qualities:qualities,currentQuality:detailCurrentCfQuality,onChange:async(quality,whepUrl)=>{detailCurrentCfQuality=quality,localStorage.setItem("cf_selected_quality",quality);const video=document.getElementById("livePlayer");if(video&&whepUrl)try{"function"==typeof cleanupCloudflareWhepPlayer&&cleanupCloudflareWhepPlayer(),video.srcObject=null,video.src="",await initCloudflareWhepPlayer(video,whepUrl),video.play().catch(e=>console.warn("[DetailPlayer] 自动播放失败:",e.message))}catch(e){console.error("[DetailPlayer] 切换清晰度失败:",e.message)}}}),options.length}function hideDetailCfQualitySelector(){const selector=document.getElementById("detailCfQualitySelector");selector&&selector.remove(),detailCfQualitySelectorInstance=null}async function getIvsPlaybackUrl(matchId){try{const metaPlayUrl=document.querySelector('meta[name="match-play-url"]')?.content;if(metaPlayUrl&&metaPlayUrl.includes(".m3u8"))return metaPlayUrl;const response=await fetch(`/api/stream/info/${matchId}`);if(!response.ok)return console.warn("[DetailPlayer] 获取流信息失败:",response.status),null;const data=await response.json();return data.playUrl||data.playbackUrl||null}catch(e){return console.error("[DetailPlayer] 获取 IVS 播放地址异常:",e.message),null}}async function loadIvsStream(playbackUrl,matchId=null){const video=document.getElementById("livePlayer");if(!video)return;const container=video.closest(".live-player-container"),setLoading=isLoading=>{container&&(isLoading?container.classList.add("loading"):container.classList.remove("loading"))};hlsInstance&&(hlsInstance.destroy(),hlsInstance=null),ivsPlayerAdapter&&(ivsPlayerAdapter.destroy(),ivsPlayerAdapter=null,window.ivsPlayerAdapter=null),hideErrorOverlay(),playerRetryCount=0,setLoading(!0);try{ivsPlayerAdapter=new IVSPlayerAdapter(video,{autoplay:!0,muted:!0,lowLatency:!0,showQualitySelector:!0,logLevel:"info"}),window.ivsPlayerAdapter=ivsPlayerAdapter,ivsPlayerAdapter.on("playing",()=>{setLoading(!1),hideErrorOverlay(),clearLoadTimeout()}),ivsPlayerAdapter.on("buffering",()=>{}),ivsPlayerAdapter.on("error",error=>{console.error("[DetailPlayer] IVS 错误:",error),error.recoverable||(setLoading(!1),fallbackToHls(matchId))}),ivsPlayerAdapter.on("ended",()=>{showErrorOverlay(document.body.getAttribute("data-stream-ended-text")||"直播已结束",!1)}),setLoadTimeout(),await ivsPlayerAdapter.initialize(playbackUrl)}catch(error){console.error("[DetailPlayer] IVS 初始化失败:",error.message),setLoading(!1),fallbackToHls(matchId)}}async function fallbackToHls(matchId){if(ivsPlayerAdapter&&(ivsPlayerAdapter.destroy(),ivsPlayerAdapter=null,window.ivsPlayerAdapter=null),matchId){const hlsUrl=`/hls/${matchId}/index.m3u8`;await loadHlsStream(hlsUrl,matchId)}}async function tryLoadDetailHlsStream(matchId){const activeSource=document.querySelector(".source-btn.active"),sourceUrl=activeSource?.dataset?.sourceUrl,hlsUrl=sourceUrl||`/hls/${matchId}/index.m3u8`;
1try{return!!(await fetch(hlsUrl,{method:"HEAD"})).ok&&(loadHlsStream(hlsUrl,matchId),new Promise(resolve=>{const timeout=setTimeout(()=>{resolve(!1)},5e3),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)}))}catch(e){return!1}}async function tryLoadDetailWebRtcStream(matchId){try{const checkResponse=await fetch(`/match/stream-check/${matchId}`),checkResult=await checkResponse.json();if(checkResult&&(!0===checkResult.notInTimeWindow||!1===checkResult.canStart)&&Number(checkResult.publishers??0)<=0&&!0!==checkResult.success){detailNotInTimeWindow=!0;const msgKey=checkResult.windowMessage||"player.notLiveYet";return showErrorOverlay(window.L?.T?window.L.T(msgKey):"比赛暂未开始直播",!1),!1}const startResult=await startStream(matchId);if(!startResult.success){const isNotLiveYet=!0===startResult.notInTimeWindow||"not_in_window"===startResult.status||"player.notLiveYet"===startResult.message||startResult.message?.includes("notLiveYet"),isStreamerNotStarted="failed"===startResult.status&&("player.streamerNotStarted"===startResult.message||startResult.message?.includes("streamerNotStarted")||startResult.message?.includes("主播未开始推流"));return isNotLiveYet||isStreamerNotStarted?(detailNotInTimeWindow=!0,showErrorOverlay(startResult.message?translateServerMessage(startResult.message):window.L?.T?window.L.T("match.player.notLiveYet"):"比赛暂未开始直播",!1),!1):(detailNotInTimeWindow=!1,startResult.message?translateServerMessage(startResult.message):window.L?.T&&window.L.T("match.player.streamStartFailed"),!1)}const isWebRtc="WEBRTC"===String(startResult?.streamType||"").toUpperCase(),publishers=Number(startResult?.publishers??0),status=startResult?.status;if(!isWebRtc||!(publishers>0||"ready"===status)){if(isWebRtc&&"starting"===status){showStreamStartingOverlay();let pollCount=0;const maxPolls=13,pollInterval=setInterval(async()=>{pollCount++;try{const checkResult=await checkStreamStatus(matchId);if(Number(checkResult.publishers??0)>0)return clearInterval(pollInterval),hideStreamStartingOverlay(),void loadWebRtcStream(matchId,!0)}catch(e){console.error("[DetailPlayer] 播放尝试异常:",e)}if(pollCount>=maxPolls){clearInterval(pollInterval),hideStreamStartingOverlay();try{const finalCheck=await checkStreamStatus(matchId);Number(finalCheck.publishers??0)>0?loadWebRtcStream(matchId,!0):showErrorOverlay(window.L?.T?window.L.T("match.player.streamTimeout"):"直播启动超时,请稍后重试",!0)}catch(e){console.error("[DetailPlayer] 最终状态检查异常:",e),showErrorOverlay(window.L?.T?window.L.T("match.player.streamTimeout"):"直播启动超时,请稍后重试",!0)}}},2e3);return!1}return startResult.message,!1}try{return await loadWebRtcStream(matchId,!0),!0}catch(e){return!1}const tokenResponse=await fetch(`/api/webrtc/token/${matchId}`);if(!tokenResponse.ok)return!1;const tokenData=await tokenResponse.json();return!(!tokenData.success||!tokenData.token)&&(loadWebRtcStream(matchId),new Promise(resolve=>{const timeout=setTimeout(()=>{resolve(!1)},5e3),video=document.getElementById("livePlayer");if(video){const onPlaying=()=>{clearTimeout(timeout),video.removeEventListener("playing",onPlaying),hideErrorOverlay(),detailIsStreamPlaying=!0,resolve(!0)};video.addEventListener("playing",onPlaying)}else clearTimeout(timeout),resolve(!1)}))}catch(e){return!1}}function startDetailStreamPolling(matchId,streamingType){stopDetailStreamPolling(),detailStreamPollingTimer=setInterval(async()=>{if(detailIsStreamPlaying)stopDetailStreamPolling();else if(document.hidden)window.PcPlaybackVisibility?.deferAutoPlay();else if("WEBRTC"===streamingType?.toUpperCase()){const statusResult=await checkStreamStatus(matchId);if(Number(statusResult?.publishers??0)>0){if(window.PcPlaybackVisibility?.deferAutoPlay())return;await tryLoadDetailStreamSilent(matchId,streamingType)&&(detailIsStreamPlaying=!0,stopDetailStreamPolling())}}else{if(window.PcPlaybackVisibility?.deferAutoPlay())return;await tryLoadDetailStreamSilent(matchId,streamingType)&&(detailIsStreamPlaying=!0,stopDetailStreamPolling())}},5e3)}function stopDetailStreamPolling(){detailStreamPollingTimer&&(clearInterval(detailStreamPollingTimer),detailStreamPollingTimer=null)}function toggleLivePlayer(){const playerSection=document.getElementById("livePlayerSection"),watchBtn=document.querySelector(".btn-live-watch"),video=document.getElementById("livePlayer");if(playerSection)if(playerSection.classList.contains("collapsed")){playerSection.classList.remove("collapsed"),watchBtn&&(watchBtn.innerHTML='<i class="fas fa-times"></i>收起直播');const hasVideo="true"===document.querySelector('meta[name="match-has-video"]')?.content,hasStreamSources="true"===document.querySelector('meta[name="match-has-stream-sources"]')?.content,streamingType=document.querySelector('meta[name="match-streaming-type"]')?.content||"null";if(hasVideo||hasStreamSources){const matchIdMeta=document.querySelector('meta[name="match-id"]');matchIdMeta&&loadStreamWithRetry(matchIdMeta.content,streamingType)}else showErrorOverlay(window.L?window.L.T("match.noLiveStream"):"暂无直播源",!1);window.dispatchEvent(new Event("livePlayerStarted"))}else playerSection.classList.add("collapsed"),watchBtn&&(watchBtn.innerHTML='<i class="fas fa-play"></i>观看直播'),stopDetailStreamPolling(),detailIsStreamPlaying=!1,video&&video.pause(),hlsInstance&&hlsInstance.stopLoad()}let fullscreenChatOriginalParent=null,fullscreenChatOriginalNextSibling=null;function togglePlayerVideoFullscreen(){const playerSection=document.getElementById("livePlayerSection"),playerContainer=document.getElementById("livePlayerContainer")||playerSection?.querySelector(".live-player
1-container"),video=document.getElementById("livePlayer");if(!playerContainer)return;const getFsEl=()=>document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement||document.msFullscreenElement||null,enterVideo=()=>{if("function"==typeof window.toggleMobileVideoFullscreen&&"function"==typeof window.isMobilePlaybackDevice&&window.isMobilePlaybackDevice())return void window.toggleMobileVideoFullscreen(playerContainer,video);const fsEl=getFsEl();if(fsEl===playerContainer||fsEl&&playerContainer.contains(fsEl))return void(()=>{const exit=document.exitFullscreen||document.webkitExitFullscreen||document.mozCancelFullScreen||document.msExitFullscreen;if(!exit||!getFsEl())return Promise.resolve();try{return Promise.resolve(exit.call(document)).catch(()=>{})}catch(_){return Promise.resolve()}})();const request=playerContainer.requestFullscreen||playerContainer.webkitRequestFullscreen||playerContainer.mozRequestFullScreen||playerContainer.msRequestFullscreen;if(request)try{Promise.resolve(request.call(playerContainer)).catch(()=>{})}catch(_){}};playerSection?.classList.contains("fullscreen")?Promise.resolve(exitFullscreen()).finally(enterVideo):enterVideo()}function togglePlayerFullscreen(){const playerSection=document.getElementById("livePlayerSection"),playerWrapper=playerSection?.querySelector(".live-player-wrapper"),playerContainer=playerSection?.querySelector(".live-player-container");if(!playerSection||!playerWrapper)return;const isFullscreen=playerSection.classList.contains("fullscreen"),body=document.body,scrollY=window.scrollY;if(!isFullscreen){const fsEl=document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement||document.msFullscreenElement;if(fsEl&&playerContainer&&(fsEl===playerContainer||playerContainer.contains(fsEl))){const exit=document.exitFullscreen||document.webkitExitFullscreen||document.mozCancelFullScreen||document.msExitFullscreen;return void Promise.resolve(exit?exit.call(document):null).catch(()=>{}).finally(()=>{togglePlayerFullscreen()})}}if(isFullscreen)exitFullscreen();else{"function"==typeof window.setMobileFsMode&&window.setMobileFsMode("chat");const applyChatFullscreenUi=()=>{playerSection.classList.add("fullscreen"),body.classList.add("live-player-fullscreen-open"),document.documentElement.classList.add("live-player-fullscreen-open"),body.style.top=`-${scrollY}px`,playerSection.dataset.scrollY=scrollY,setupFullscreenControls(playerContainer),moveChatIntoFullscreen(playerWrapper)},elem=playerSection,requestFs=elem.requestFullscreen||elem.webkitRequestFullscreen||elem.mozRequestFullScreen||elem.msRequestFullscreen;if(requestFs)try{const result=requestFs.call(elem);Promise.resolve(result).then(applyChatFullscreenUi).catch(()=>{enterCssFullscreen()})}catch(_){enterCssFullscreen()}else enterCssFullscreen()}}function moveChatIntoFullscreen(playerWrapper){if(!playerWrapper)return;const chatRoom=document.querySelector(".chat-room-container");if(!chatRoom||chatRoom.classList.contains("in-fullscreen"))return;fullscreenChatOriginalParent=chatRoom.parentElement,fullscreenChatOriginalNextSibling=chatRoom.nextElementSibling;let chatContainer=playerWrapper.querySelector(".fullscreen-chat-container");chatContainer||(chatContainer=document.createElement("div"),chatContainer.className="fullscreen-chat-container",playerWrapper.appendChild(chatContainer)),chatContainer.appendChild(chatRoom),chatRoom.classList.add("in-fullscreen");const chatMessages=chatRoom.querySelector("#chatMessages");chatMessages&&requestAnimationFrame(()=>{chatMessages.scrollTop=chatMessages.scrollHeight})}function enterCssFullscreen(){const playerSection=document.getElementById("livePlayerSection"),playerContainer=playerSection?.querySelector(".live-player-container"),playerWrapper=playerSection?.querySelector(".live-player-wrapper"),body=document.body,scrollY=window.scrollY;playerSection&&playerWrapper&&(playerSection.classList.add("fullscreen"),body.classList.add("live-player-fullscreen-open"),document.documentElement.classList.add("live-player-fullscreen-open"),body.style.top=`-${scrollY}px`,playerSection.dataset.scrollY=scrollY,setupFullscreenControl
1s(playerContainer),moveChatIntoFullscreen(playerWrapper))}function setupFullscreenControls(playerContainer){if(!playerContainer||playerContainer.querySelector(".fullscreen-exit-btn"))return;const exitBtn=document.createElement("button");exitBtn.className="fullscreen-exit-btn",exitBtn.innerHTML='<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',exitBtn.title=window.L?.T?window.L.T("common.close"):"关闭",exitBtn.setAttribute("aria-label","关闭全屏");let hideTimeout=null;function showExitButton(){clearTimeout(hideTimeout),exitBtn.classList.add("show"),hideTimeout=setTimeout(()=>{exitBtn.classList.remove("show")},3e3)}function hideExitButton(){clearTimeout(hideTimeout),exitBtn.classList.remove("show")}playerContainer.addEventListener("click",function(e){e.target===exitBtn||exitBtn.contains(e.target)||showExitButton()}),playerContainer.addEventListener("touchstart",function(e){e.target===exitBtn||exitBtn.contains(e.target)||showExitButton()}),exitBtn.onclick=function(e){e.stopPropagation(),togglePlayerFullscreen()},exitBtn.addEventListener("touchstart",function(e){e.stopPropagation(),e.preventDefault(),togglePlayerFullscreen()}),playerContainer.addEventListener("mouseleave",hideExitButton),playerContainer.addEventListener("touchend",function(e){setTimeout(()=>{exitBtn.matches(":hover")||hideExitButton()},2e3)}),playerContainer.appendChild(exitBtn)}function exitFullscreen(){const playerSection=document.getElementById("livePlayerSection"),body=document.body;if(!playerSection)return Promise.resolve();let nativeExit=Promise.resolve();if(document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement||document.msFullscreenElement){const exitFs=document.exitFullscreen||document.webkitExitFullscreen||document.mozCancelFullScreen||document.msExitFullscreen;if(exitFs)try{nativeExit=Promise.resolve(exitFs.call(document)).catch(()=>{})}catch(_){nativeExit=Promise.resolve()}}playerSection.classList.remove("fullscreen"),body.classList.remove("live-player-fullscreen-open"),document.documentElement.classList.remove("live-player-fullscreen-open"),"function"==typeof window.setMobileFsMode&&window.setMobileFsMode("normal");const container=playerSection.querySelector(".live-player-container"),wrapper=playerSection.querySelector(".live-player-wrapper"),video=container?.querySelector("video");container&&(container.style.cssText=""),wrapper&&(wrapper.style.cssText=""),video&&(video.style.cssText="");const scrollY=playerSection.dataset.scrollY;scrollY&&(body.style.top="",window.scrollTo(0,parseInt(scrollY)),delete playerSection.dataset.scrollY);const exitBtn=playerSection.querySelector(".fullscreen-exit-btn");return exitBtn&&exitBtn.remove(),restoreChatPosition(),nativeExit}function restoreChatPosition(){const chatRoom=document.querySelector(".chat-room-container");chatRoom&&fullscreenChatOriginalParent&&(chatRoom.classList.remove("in-fullscreen"),fullscreenChatOriginalParent.insertBefore(chatRoom,fullscreenChatOriginalNextSibling),fullscreenChatOriginalParent=null,fullscreenChatOriginalNextSibling=null)}function togglePiP(){const video=document.getElementById("livePlayer");video&&document.pictureInPictureEnabled&&(document.pictureInPictureElement?document.exitPictureInPicture():video.requestPictureInPicture().catch(e=>console.log("画中画失败:",e)))}function shareLive(){const url=window.location.href,title=document.title;"function"!=typeof shareUrlWithFallback?navigator.share?navigator.share({title:title,url:url}).catch(()=>{}):navigator.clipboard&&navigator.clipboard.writeText(url).catch(()=>{}):shareUrlWithFallback(url,title)}function scrollFocusCards(direction,containerId){const container=document.getElementById(containerId);if(!container||container.classList.contains("d-none"))return;const card=container.querySelector(".focus-card, .focus-match-card"),distance=2*((card?.offsetWidth||240)+8);container.scrollBy({left:"left"===direction?-distance:distance,behavior:"smooth"}),setTimeout(()=>{updateFocusCardsArrows(containerId),"function"==typeof window.updateFocusCardsScrollbar&&window.updateFocusCardsScrollbar(container),"function"==typeof window.updateFocusNavArrows&&window.updateFocusNavArrows(container)},300)}function updateFocusCardsArrows(containerId){const container=document.getElementById(containerId);if(!container)return;const wrapper=container.closest(".focus-cards-wrapper");if(!wrapper)return;const leftArrow=wrapper.querySelector(".focus-cards-nav-left"),rightArrow=wrapper.querySelector(".focus-cards-nav-right");if(!leftArrow||!rightArrow)return;const hasCards=container.querySelectorAll(".focus-card, .focus-match-card").length>0,isOverflowing=container.scrollWidth>container.clientWidth+1;if(!hasCards||!isOverflowing)return leftArrow.style.display="none",void(rightArrow.style.display="none");const isAtStart=container.scrollLeft<=1,isAtEnd=container.scrollLeft+container.clientWidth>=container.scrollWidth-1;isAtStart?leftArrow.style.display="none":(leftArrow.style.display="flex",leftArrow.style.opacity="1",leftArrow.style.cursor="pointer"),isAtEnd?rightArrow.style.display="none":(rightArrow.style.display="flex",rightArrow.style.opacity="1",rightArrow.style.cursor="pointer")}function initFocusCardsArrows(){["liveMatchCards","upcomingMatchCards"].forEach(containerId=>{const container=document.getElementById(containerId);if(container){container.addEventListener("scroll",()=>updateFocusCardsArrows(containerId),{passive:!0});const resizeHandler=()=>{setTimeout(()=>updateFocusCardsArrows(containerId),50)};window.addEventListener("resize",resizeHandler,{passive:!0})}}),setTimeout(()=>{["liveMatchCards","upcomingMatchCards"].forEach(containerId=>{updateFocusCardsArrows(containerId);const container=document.getElementById(containerId);container&&"function"==typeof window.updateFocusCardsScrollbar&&window.updateFocusCardsScrollbar(container)})},100),setTimeout(()=>{["liveMatchCards","upcomingMatchCards"].forEach(containerId=>{updateFocusCardsArrows(containerId)})},500)}function initPageVisibilityRefresh(){window.PageVisibilityManager}window.togglePlayerVideoFullscreen=togglePlayerVideoFullscreen,window.togglePlayerFullscreen=togglePlayerFullscreen,window.exitPlayerFullscreen=exitFullscreen,document.addEventListener("keydown",function(e){if("Escape"===e.key){const playerSection=document.getElementById("livePlayerSection");playerSection&&playerSection.classList.contains("fullscreen")&&togglePlayerFullscreen()}}),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",initFocusCardsArrows):initFocusCardsArrows(),function ensureCustomSelectAssets(){if(!document.querySelector('link[href*="custom-select.css"]')){var link=document.createElement("link");link.rel="stylesheet",link.href="/css/custom-select.css",document.head.appendChild(link)}if(!document.querySelector('script[src*="custom-select.js"]')){var script=document.createElement("script");script.src="/js/custom-select.js",document.head.appendChild(script)}}(),window.MatchStore=function(){const LOG_LEVEL=0|(localStorage.getItem("matchStore_logLevel")||"3"),log_error=(...args)=>LOG_LEVEL>=1&&console.error("[MatchStore]",...args),log_warn=(...args)=>LOG_LEVEL>=2&&console.warn("[MatchStore]",...args),log_info=(...args)=>LOG_LEVEL>=3&&console.log("[MatchStore]",...args),log_debug=(...args)=>LOG_LEVEL>
1=4&&console.log("[MatchStore:debug]",...args);let serverEpoch=null,isInitialized=!1,cleanupTimer=null;const byMatchId=new Map;function getEventMinute(e){if(!e)return 0;const m=e.minute??e.Minute??e.elapsed??e.Elapsed??e.time?.elapsed??e.time?.Elapsed??0,n="string"==typeof m?parseInt(m,10):m;return Number.isFinite(n)?n:0}function isOldestFirst(events){if(!Array.isArray(events)||events.length<2)return!0;const first=getEventMinute(events[0]),last=getEventMinute(events[events.length-1]);return first===last||first<last}function tagEventsWithSeq(matchData,events,assumedOldestFirst=!0){return function ensureEventSeqState(matchData){matchData&&"number"!=typeof matchData.__eventSeqCounter&&(matchData.__eventSeqCounter=0)}(matchData),Array.isArray(events)&&0!==events.length?((assumedOldestFirst?events:[...events].reverse()).forEach(ev=>{ev&&"object"==typeof ev&&(matchData.__eventSeqCounter+=1,ev._seq=matchData.__eventSeqCounter)}),events):events}const versions=new Map,lastUpdated=new Map,pendingCleanup=new Map,subscribers=[],PatchKeys={StatusShort:"status.short",StatusLong:"status.long",Minute:"minute",MatchDate:"matchDate",MatchTimestamp:"matchTimestamp",ScoreHome:"score.home",ScoreAway:"score.away",ScoreHalftimeHome:"score.halftime.home",ScoreHalftimeAway:"score.halftime.away",ScoreFulltimeHome:"score.fulltime.home",ScoreFulltimeAway:"score.fulltime.away",TeamHomeId:"team.home.id",TeamHomeName:"team.home.name",TeamHomeLogo:"team.home.logo",TeamAwayId:"team.away.id",TeamAwayName:"team.away.name",TeamAwayLogo:"team.away.logo",LeagueId:"league.id",LeagueApiId:"league.apiId",LeagueName:"league.name",LeagueLogo:"league.logo",LeagueCountry:"league.country",LeagueSeason:"league.season",LeagueRound:"league.round",VenueName:"venue.name",VenueCity:"venue.city",StatsHomePossession:"stats.home.possession",StatsAwayPossession:"stats.away.possession",StatsHomeShots:"stats.home.shots",StatsAwayShots:"stats.away.shots",StatsHomeShotsOnTarget:"stats.home.shotsOnTarget",StatsAwayShotsOnTarget:"stats.away.shotsOnTarget",StatsHomeCorners:"stats.home.corners",StatsAwayCorners:"stats.away.corners",StatsHomeFouls:"stats.home.fouls",StatsAwayFouls:"stats.away.fouls",StatsHomeYellowCards:"stats.home.yellowCards",StatsAwayYellowCards:"stats.away.yellowCards",StatsHomeRedCards:"stats.home.redCards",StatsAwayRedCards:"stats.away.redCards",StatsHomeOffsides:"stats.home.offsides",StatsHomePasses:"stats.home.passes",StatsHomePassAccuracy:"stats.home.passAccuracy",StatsAwayOffsides:"stats.away.offsides",StatsAwayPasses:"stats.away.passes",StatsAwayPassAccuracy:"stats.away.passAccuracy",IsLive:"isLive",IsHot:"isHot",ViewerCount:"viewerCount",Events:"events",EventsHash:"eventsHash",NewEvents:"newEvents",EventsCount:"eventsCount"};async function init(scope="live"){log_info("初始化, scope:",scope);try{const response=await fetch(`/api/fixtures/snapshot?scope=${scope}`);if(!response.ok)throw new Error(`HTTP ${response.status}`);const data=await response.json();if(!data.success)throw new Error(data.message||"获取快照失败");const oldEpoch=function loadServerEpoch(){try{return localStorage.getItem("matchStore_serverEpoch")}catch(e){return null}}();return oldEpoch&&oldEpoch!==data.serverEpoch&&(log_info("serverEpoch 变化,清空 store:",oldEpoch,"->",data.serverEpoch),clear()),serverEpoch=data.serverEpoch,saveServerEpoch(serverEpoch),initSnapshot(data),isInitialized=!0,startCleanupTimer(),log_info("初始化完成, matches:",byMatchId.size,"epoch:",serverEpoch),!0}catch(error){return log_error("初始化失败:",error),!1}}function initSnapshot(snapshot){const{matches:matches,versions:serverVersions}=snapshot;byMatchId.clear(),versions.clear(),matches&&Array.isArray(matches)&&matches.forEach(match=>{const matchData=match.data||{};if(matchData&&matchData[PatchKeys.Events]&&Array.isArray(matchData[PatchKeys.Events])){const oldestFirst=isOldestFirst(matchData[PatchKeys.Events]);tagEventsWithSeq(matchData,matchData[PatchKeys.Events],oldestFirst)}byMatchId.set(match.matchId,matchData),versions.set(match.matchId,match.version||1)}),saveVersions(),notifySubscribers({type:"snapshot",matches:Array.from(byMatchId.keys())})}function applyPatch(matchId,version,patch,msgServerEpoch){if(msgServerEpoch&&serverEpoch&&msgServerEpoch!==serverEpoch)return console.warn("[MatchStore] serverEpoch 不匹配,需要重同步:",serverEpoch,"->",msgServerEpoch),triggerResync("epoch_mismatch"),!1;const lastVersion=versions.get(matchId)||0;if(version<=lastVersion)return log_debug("丢弃旧版本 Patch:",{matchId:matchId,lastVersion:lastVersion,version:version}),!1;if(version>lastVersion+1)return log_warn("版本跳跃,触发重同步:",{matchId:matchId,lastVersion:lastVersion,version:version}),triggerResync("version_gap",matchId),!1;let matchData=byMatchId.get(matchId);matchData||(matchData={},byMatchId.set(matchId,matchData));const changedKeys=[];for(const[key,value]of Object.entries(patch))if(key===PatchKeys.NewEvents&&Array.isArray(value)){const existingEvents=matchData[PatchKeys.Events]||[];tagEventsWithSeq(matchData,value,isOldestFirst(value));const mergedEvents=Array.isArray(existingEvents)?[...value,...existingEvents]:value;matchData[PatchKeys.Events]=mergedEvents,changedKeys.push(PatchKeys.Events),log_debug("合并增量事件:",{matchId:matchId,newCount:value.length,totalCount:mergedEvents.length})}else if(key===PatchKeys.Events&&Array.isArray(value)){const oldestFirst=isOldestFirst(value);
1matchData[PatchKeys.Events]=tagEventsWithSeq(matchData,value,oldestFirst),changedKeys.push(PatchKeys.Events)}else if(key===PatchKeys.EventsCount){const currentCount=(matchData[PatchKeys.Events]||[]).length;currentCount!==value&&log_warn("事件数量不一致,可能需要重同步:",{matchId:matchId,local:currentCount,server:value})}else matchData[key]!==value&&(matchData[key]=value,changedKeys.push(key));return versions.set(matchId,version),lastUpdated.set(matchId,Date.now()),saveVersions(),log_debug("Patch 已应用:",{matchId:matchId,version:version,changedKeys:changedKeys}),notifySubscribers({type:"patch",matchId:matchId,version:version,changedKeys:changedKeys,patch:patch}),triggerUIUpdate(matchId,patch,changedKeys),!0}async function triggerResync(reason,matchId=null){if(log_info("触发重同步:",{reason:reason,matchId:matchId}),matchId)try{const response=await fetch(`/api/fixtures/snapshot?matchIds=${matchId}`);if(response.ok){const data=await response.json();if(data.success&&data.matches&&data.matches.length>0){const match=data.matches[0];byMatchId.set(match.matchId,match.data||{}),versions.set(match.matchId,match.version||1),serverEpoch=data.serverEpoch,saveServerEpoch(serverEpoch),saveVersions();const matchData=match.data||{};triggerUIUpdate(match.matchId,matchData,Object.keys(matchData))}}}catch(e){log_error("重同步失败:",e)}else{await init("live");for(const[mid,matchData]of byMatchId.entries())triggerUIUpdate(mid,matchData,Object.keys(matchData))}}let pendingUIUpdates=new Map,rafScheduled=!1;function triggerUIUpdate(matchId,patch,changedKeys){const payload=function buildPayloadFromPatch(matchId,patch){const data=byMatchId.get(matchId)||{};return{matchId:matchId,status:patch[PatchKeys.StatusShort]??data[PatchKeys.StatusShort],minute:patch[PatchKeys.Minute]??data[PatchKeys.Minute],homeScore:patch[PatchKeys.ScoreHome]??data[PatchKeys.ScoreHome],awayScore:patch[PatchKeys.ScoreAway]??data[PatchKeys.ScoreAway],homeTeam:patch[PatchKeys.TeamHomeName]??data[PatchKeys.TeamHomeName],awayTeam:patch[PatchKeys.TeamAwayName]??data[PatchKeys.TeamAwayName],homeTeamLogo:patch[PatchKeys.TeamHomeLogo]??data[PatchKeys.TeamHomeLogo],awayTeamLogo:patch[PatchKeys.TeamAwayLogo]??data[PatchKeys.TeamAwayLogo],leagueName:patch[PatchKeys.LeagueName]??data[PatchKeys.LeagueName],leagueLogo:patch[PatchKeys.LeagueLogo]??data[PatchKeys.LeagueLogo],homePossession:patch[PatchKeys.StatsHomePossession]??data[PatchKeys.StatsHomePossession],awayPossession:patch[PatchKeys.StatsAwayPossession]??data[PatchKeys.StatsAwayPossession],homeShots:patch[PatchKeys.StatsHomeShots]??data[PatchKeys.StatsHomeShots],awayShots:patch[PatchKeys.StatsAwayShots]??data[PatchKeys.StatsAwayShots],homeShotsOnTarget:patch[PatchKeys.StatsHomeShotsOnTarget]??data[PatchKeys.StatsHomeShotsOnTarget],awayShotsOnTarget:patch[PatchKeys.StatsAwayShotsOnTarget]??data[PatchKeys.StatsAwayShotsOnTarget],homeCorners:patch[PatchKeys.StatsHomeCorners]??data[PatchKeys.StatsHomeCorners],awayCorners:patch[PatchKeys.StatsAwayCorners]??data[PatchKeys.StatsAwayCorners],homeFouls:patch[PatchKeys.StatsHomeFouls]??data[PatchKeys.StatsHomeFouls],awayFouls:patch[PatchKeys.StatsAwayFouls]??data[PatchKeys.StatsAwayFouls],homeYellowCards:patch[PatchKeys.StatsHomeYellowCards]??data[PatchKeys.StatsHomeYellowCards],awayYellowCards:patch[PatchKeys.StatsAwayYellowCards]??data[PatchKeys.StatsAwayYellowCards],homeRedCards:patch[PatchKeys.StatsHomeRedCards]??data[PatchKeys.StatsHomeRedCards],awayRedCards:patch[PatchKeys.StatsAwayRedCards]??data[PatchKeys.StatsAwayRedCards],homeOffsides:patch[PatchKeys.StatsHomeOffsides]??data[PatchKeys.StatsHomeOffsides],awayOffsides:patch[PatchKeys.StatsAwayOffsides]??data[PatchKeys.StatsAwayOffsides],homePasses:patch[PatchKeys.StatsHomePasses]??data[PatchKeys.StatsHomePasses],awayPasses:patch[PatchKeys.StatsAwayPasses]??data[PatchKeys.StatsAwayPasses],homePassAccuracy:patch[PatchKeys.StatsHomePassAccuracy]??data[PatchKeys.StatsHomePassAccuracy],awayPassAccuracy:patch[PatchKeys.StatsAwayPassAccuracy]??data[PatchKeys.StatsAwayPassAccuracy],isLive:patch[PatchKeys.IsLive]??data[PatchKeys.IsLive],events:patch[PatchKeys.Events]??data[PatchKeys.Events]}}(matchId,patch);pendingUIUpdates.set(matchId,{payload:payload,changedKeys:changedKeys}),rafScheduled||(rafScheduled=!0,requestAnimationFrame(flushUIUpdates))}function flushUIUpdates(){if(rafScheduled=!1,0===pendingUIUpdates.size)return;const startTime=performance.now();log_debug(`批量更新 ${pendingUIUpdates.size} 个比赛的 UI`);
1for(const[matchId,{payload:payload,changedKeys:changedKeys}]of pendingUIUpdates)if("function"==typeof window.updateScoreDisplay&&window.updateScoreDisplay(matchId,payload),"function"==typeof window.updateMatchInfo&&window.updateMatchInfo(matchId,payload),"function"==typeof window.updateStatsDisplay&&window.updateStatsDisplay(matchId,payload),"function"==typeof window.updateMatchStatus&&window.updateMatchStatus(matchId,payload),"function"==typeof window.updateEventsDisplay&&changedKeys.includes(PatchKeys.Events)&&window.updateEventsDisplay(matchId,payload),changedKeys.includes(PatchKeys.Minute)&&window.MatchLiveHub){const minute=payload.minute;window.MatchLiveHub.MinuteTicker&&window.MatchLiveHub.MinuteTicker.setMinute(matchId,minute)}pendingUIUpdates.clear();const duration=performance.now()-startTime;log_debug(`批量更新完成,耗时: ${duration.toFixed(2)}ms`)}function getAllMatches(){const result=[];return byMatchId.forEach((data,matchId)=>{result.push({matchId:matchId,version:versions.get(matchId)||1,data:{...data}})}),result}function getLiveMatches(){return getAllMatches().filter(m=>!0===m.data[PatchKeys.IsLive])}function clear(){byMatchId.clear(),versions.clear(),saveVersions()}function notifySubscribers(event){subscribers.forEach(cb=>{try{cb(event)}catch(e){log_error("订阅者回调错误:",e)}})}function saveServerEpoch(epoch){try{localStorage.setItem("matchStore_serverEpoch",epoch)}catch(e){}}function saveVersions(){try{const obj={};versions.forEach((v,k)=>obj[k]=v),localStorage.setItem("matchStore_versions",JSON.stringify(obj))}catch(e){}}function startCleanupTimer(){cleanupTimer||(cleanupTimer=setInterval(()=>{runCleanup()},3e5),log_debug("清理定时器已启动"))}function runCleanup(){const now=Date.now();let cleanedCount=0;pendingCleanup.forEach((cleanupTime,matchId)=>{now>=cleanupTime&&(removeMatch(matchId),pendingCleanup.delete(matchId),cleanedCount++)});const staleThreshold=now-216e5;lastUpdated.forEach((updateTime,matchId)=>{if(updateTime<staleThreshold){const data=byMatchId.get(matchId);data&&!0===data[PatchKeys.IsLive]||(removeMatch(matchId),cleanedCount++)}}),cleanedCount>0&&log_info("清理完成:",{cleanedCount:cleanedCount,remaining:byMatchId.size})}function removeMatch(matchId){byMatchId.delete(matchId),versions.delete(matchId),lastUpdated.delete(matchId),pendingCleanup.delete(matchId),saveVersions(),log_debug("已清理比赛:",matchId)}return{init:init,initSnapshot:initSnapshot,applyPatch:applyPatch,handleBatchUpdates:function handleBatchUpdates(updates){Array.isArray(updates)&&0!==updates.length?(log_info("处理批量更新:",updates.length),updates.forEach(update=>{if(!update||!update.matchId)return void log_warn("无效的更新数据:",update);const{matchId:matchId}=update,patch={};void 0!==update.status&&(patch[PatchKeys.StatusShort]=update.status),void 0!==update.homeScore&&(patch[PatchKeys.ScoreHome]=update.homeScore),void 0!==update.awayScore&&(patch[PatchKeys.ScoreAway]=update.awayScore),void 0!==update.minute&&(patch[PatchKeys.Minute]=update.minute),void 0!==update.homeTeam&&(patch[PatchKeys.TeamHomeName]=update.homeTeam),void 0!==update.awayTeam&&(patch[PatchKeys.TeamAwayName]=update.awayTeam),applyPatch(matchId,0,patch,null)})):log_debug("批量更新为空")},handleMatchStarted:function handleMatchStarted(message){const{matchId:matchId,version:version,serverEpoch:msgEpoch,preview:preview}=message;if(msgEpoch&&serverEpoch&&msgEpoch!==serverEpoch)triggerResync("epoch_mismatch");else{if(log_info("比赛开始:",{matchId:matchId,preview:preview}),preview){const matchData={[PatchKeys.StatusShort]:preview.status||"1H",[PatchKeys.IsLive]:!0,[PatchKeys.TeamHomeName]:preview.homeTeam?.name,[PatchKeys.TeamHomeLogo]:preview.homeTeamLogo||preview.homeTeam?.logo,[PatchKeys.TeamAwayName]:preview.awayTeam?.name,[PatchKeys.TeamAwayLogo]:preview.awayTeamLogo||preview.awayTeam?.logo,[PatchKeys.LeagueName]:preview.league?.name,[PatchKeys.LeagueLogo]:preview.leagueLogo||preview.league?.logo,[PatchKeys.MatchDate]:preview.matchDate};
1byMatchId.set(matchId,matchData),versions.set(matchId,version||1)}notifySubscribers({type:"matchStarted",matchId:matchId,preview:preview})}},triggerResync:triggerResync,get:function get(matchId,key=null){const data=byMatchId.get(matchId);return data?key?data[key]:{...data}:null},getAllMatches:getAllMatches,getLiveMatches:getLiveMatches,getVersion:function getVersion(matchId){return versions.get(matchId)||0},getServerEpoch:function getServerEpoch(){return serverEpoch},clear:clear,subscribe:function subscribe(callback){return subscribers.push(callback),()=>{const index=subscribers.indexOf(callback);index>-1&&subscribers.splice(index,1)}},PatchKeys:PatchKeys,isInitialized:()=>isInitialized,getStats:function getStats(){return{matchCount:byMatchId.size,liveCount:getLiveMatches().length,pendingCleanupCount:pendingCleanup.size,serverEpoch:serverEpoch,isInitialized:isInitialized}},runCleanup:runCleanup,scheduleCleanup:function scheduleCleanup(matchId){const cleanupTime=Date.now()+6e4;pendingCleanup.set(matchId,cleanupTime),log_debug("已安排清理:",{matchId:matchId,delayMs:6e4})},removeMatch:removeMatch,startCleanupTimer:startCleanupTimer,stopCleanupTimer:function stopCleanupTimer(){cleanupTimer&&(clearInterval(cleanupTimer),cleanupTimer=null)}}}();const TEST_DATA_API_IDS=[1387836,1498765];function isTestData(apiId){return TEST_DATA_API_IDS.includes(apiId)}function checkAllActivePlayers(){const result={hasAnyActivePlayer:!1,focusPlayer:null,detailPlayer:null,protectedSections:[],activeStreamInfo:{},userHasPlayedBefore:!1};try{const focusPlayed="1"===sessionStorage.getItem("focus_user_played"),detailPlayed="1"===sessionStorage.getItem("detail_user_played");result.userHasPlayedBefore=focusPlayed||detailPlayed,result.userHasPlayedBefore}catch(e){console.warn("[checkAllActivePlayers] 检查 sessionStorage 失败:",e)}const focusPlayerSection=document.getElementById("focusLiveSection"),focusPlayer=document.getElementById("focusLivePlayer");if(focusPlayer&&focusPlayerSection){const isExpanded=focusPlayerSection.classList.contains("expanded"),hasWebRTCStream=focusPlayer.srcObject&&"function"==typeof focusPlayer.srcObject.getTracks&&focusPlayer.srcObject.getTracks().length>0,hasHLSStream=focusPlayer.src&&!focusPlayer.paused,hasAnySource=focusPlayer.srcObject||focusPlayer.src,isPlaying=!focusPlayer.paused&&!focusPlayer.ended,isPictureInPicture=document.pictureInPictureElement===focusPlayer,hasActiveHLS=window.focusHlsInstance&&!window.focusHlsInstance.destroyed,hasActiveLiveKit=window.focusLivekitRoom&&"connected"===window.focusLivekitRoom.state;(isExpanded||hasAnySource&&(isPlaying||isPictureInPicture)||hasActiveHLS||hasActiveLiveKit)&&(result.hasAnyActivePlayer=!0,result.focusPlayer={element:focusPlayer,hasWebRTCStream:hasWebRTCStream,hasHLSStream:hasHLSStream,isPlaying:isPlaying,isPictureInPicture:isPictureInPicture,hasActiveHLS:hasActiveHLS,hasActiveLiveKit:hasActiveLiveKit,isExpanded:isExpanded,streamType:window.lastFocusStreamingType,matchId:window.streamPollingMatchId},result.protectedSections.push("focusLiveSection"))}const detailPlayerSection=document.getElementById("livePlayerSection"),detailPlayer=document.getElementById("livePlayer");if(detailPlayer&&detailPlayerSection){const isExpanded=detailPlayerSection.classList.contains("expanded"),hasWebRTCStream=detailPlayer.srcObject&&detailPlayer.srcObject.getTracks().length>0,hasHLSStream=detailPlayer.src&&!detailPlayer.paused,hasAnySource=detailPlayer.srcObject||detailPlayer.src,isPlaying=!detailPlayer.paused&&!detailPlayer.ended,isPictureInPicture=document.pictureInPictureElement===detailPlayer,hasActiveHLS=window.hlsInstance&&!window.hlsInstance.destroyed,hasActiveLiveKit=window.livekitRoom&&"connected"===window.livekitRoom.state;(isExpanded||hasAnySource&&(isPlaying||isPictureInPicture)||hasActiveHLS||hasActiveLiveKit)&&(result.hasAnyActivePlayer=!0,result.detailPlayer={element:detailPlayer,hasWebRTCStream:hasWebRTCStream,hasHLSStream:hasHLSStream,isPlaying:isPlaying,isPictureInPicture:isPictureInPicture,hasActiveHLS:hasActiveHLS,hasActiveLiveKit:hasActiveLiveKit,isExpanded:isExpanded,streamType:window.lastDetailStreamingType,matchI
1d:window.detailMatchId},result.protectedSections.push("livePlayerSection"))}return result.hasAnyActivePlayer&&(result.activeStreamInfo={focusStreamMatchId:window.streamPollingMatchId,focusStreamType:window.lastFocusStreamingType,detailMatchId:window.detailMatchId,detailStreamType:window.lastDetailStreamingType,isStreamPlaying:window.isStreamPlaying,detailIsStreamPlaying:window.detailIsStreamPlaying}),result}async function silentRefreshPageContent(){}function updateLiveCountBadge(count){const navLiveCount=document.getElementById("liveCount"),homeLink=navLiveCount?.closest("a.nav-link");navLiveCount&&(navLiveCount.textContent=count,navLiveCount.style.display=0===count?"none":"inline-block"),homeLink&&(count>0?homeLink.classList.add("has-live"):homeLink.classList.remove("has-live"))}function updateFocusLiveCount(count){const focusCount="function"==typeof window.refreshFocusLiveCountFromDom?window.refreshFocusLiveCountFromDom():Math.max(0,0|count),focusLiveCount=document.getElementById("focusLiveCount");focusLiveCount&&"function"!=typeof window.refreshFocusLiveCountFromDom&&(focusLiveCount.textContent=focusCount),"function"==typeof window.updateFocusLiveButtonState&&window.updateFocusLiveButtonState(focusCount)}function updateMatchLiveBadge(change){let badgeId;badgeId=change.identifier?`match_live_badge_${change.identifier}`:change.streamSessionId?`match_live_badge_${change.matchId}_${change.streamSessionId}`:`match_live_badge_${change.matchId}`;const container=document.getElementById(badgeId);if(!container)return;const card=document.querySelector(`.focus-card[data-card-id="${change.identifier}"], .focus-match-card[data-card-id="${change.identifier}"]`),matchDate=card?.dataset?.matchDate;let timeStr="--:--";if(matchDate)try{const date=new Date(matchDate);if(!isNaN(date.getTime())){const month=String(date.getMonth()+1).padStart(2,"0"),day=String(date.getDate()).padStart(2,"0");timeStr=`${month}/${day} ${String(date.getHours()).padStart(2,"0")}:${String(date.getMinutes()).padStart(2,"0")}`}}catch(e){console.warn("解析比赛时间失败:",e)}if(change.isLive){const cardItem=card?.closest(".focus-card-item");card&&(card.classList.add("is-live"),change.playbackIdentifier&&(card.dataset.identifier=change.playbackIdentifier),cardItem&&(cardItem.dataset.isLive="true"));const isWorldCup="true"===cardItem?.dataset.isWorldCup||"function"==typeof window.isFocusWorldCupMatch&&window.isFocusWorldCupMatch(change.leagueApiId,change.league),liveBadgeLabel="function"==typeof window.buildFocusLiveBadgeLabel?window.buildFocusLiveBadgeLabel(isWorldCup,"LIVE"):'<span class="badge bg-danger">LIVE</span>';container.innerHTML=`\n            <div class="focus-card-badge">\n                ${liveBadgeLabel}\n            </div>\n            <div class="focus-card-badge focus-card-badge-right">\n                <span class="badge focus-time-badge">${timeStr}</span>\n            </div>\n        `,card&&card.classList.contains("active")&&tryPlaySelectedMatch(change)}else{if(card){card.classList.remove("is-live");const cardItem=card.closest(".focus-card-item");cardItem&&(cardItem.dataset.isLive="false")}container.innerHTML=`\n            <div class="focus-card-badge">\n                <span class="badge bg-secondary">直播结束</span>\n            </div>\n            <div class="focus-card-badge focus-card-badge-right">\n                <span class="badge focus-time-badge">${timeStr}</span>\n            </div>\n        `}"function"==typeof window.refreshFocusLiveCountFromDom&&window.refreshFocusLiveCountFromDom()}function tryPlaySelectedMatch(change){if(window.PcPlaybackVisibility?.deferAutoPlay())return;const focusPlayerContainer=document.getElementById("focusPlayerContainer");if(!focusPlayerContainer)return;const containerIdentifier=focusPlayerContainer.dataset.identifier;if(containerIdentifier!==(change.playbackIdentifier||change.identifier)&&containerIdentifier!==change.identifier)return;const isWaitingState=focusPlayerContainer.classList.contains("focus-no-stream"),hasCountdown=document.querySelector(".flip-countdown"),hasErrorOverlay=document.querySelector(".focus-not-live-overlay, .focus-error-overlay");if(!isWaitingState&&!hasCountdown&&!hasErrorOverlay)return;const videoElement=document.getElementById("focusLivePlayer");videoElement&&!videoElement.paused&&videoElement.currentTime>0||("function"==typeof window.retryFocusPlayback?window.retryFocusPlayback():console.warn("[AutoPlay] retryFocusPlayback 函数不存在"))}async function handleNewLiveStreamAdded(change){const liveCardsContainer=document.getElementById("liveMatchCards");if(!liveCardsContainer)return;liveCardsContainer.classList.remove("d-none"),liveCardsContainer.closest(".focus-live-cards")?.classList.remove("d-none");
1const cardId=change.identifier||`${change.matchId}_${change.streamSessionId||""}`,existingCard=document.querySelector(`.focus-card[data-card-id="${cardId}"], .focus-match-card[data-card-id="${cardId}"]`);if(existingCard){change.isLive&&existingCard.classList.add("is-live");const cardItem=existingCard.closest(".focus-card-item");return cardItem&&(cardItem.dataset.isLive=change.isLive?"true":"false"),updateMatchLiveBadge(change),void("function"==typeof window.sortFocusCardsByTime&&window.sortFocusCardsByTime(liveCardsContainer))}if(change.matchId)try{const response=await fetch(`/api/match/${change.matchId}/detail`);if(!response.ok)return void console.warn("[NewLiveStream] 获取比赛详情失败:",response.status);const matchData=await response.json();matchData.isManualFootball&&(change.isManualFootball=!0);const cardHtml=createFocusCardHtml(matchData,change);liveCardsContainer.insertAdjacentHTML("beforeend",cardHtml),"function"==typeof window.sortFocusCardsByTime&&window.sortFocusCardsByTime(liveCardsContainer),"function"==typeof window.applyBlvToFocusCardStreamers&&window.applyBlvToFocusCardStreamers(liveCardsContainer);const newCard=liveCardsContainer.querySelector(`.focus-card[data-card-id="${cardId}"]`);if(newCard&&(newCard.style.opacity="0",newCard.style.transform="scale(0.95)",setTimeout(()=>{newCard.style.transition="opacity 0.3s, transform 0.3s",newCard.style.opacity="1",newCard.style.transform="scale(1)"},50)),"undefined"!=typeof matchSelectorMatches&&window.matchSelectorMatches&&(window.matchSelectorMatches.push({id:matchData.id,streamSessionId:change.streamSessionId,playbackIdentifier:change.playbackIdentifier,isManualFootball:!!change.isManualFootball,homeTeam:matchData.homeTeam,awayTeam:matchData.awayTeam,homeTeamLogo:matchData.homeTeamLogo,awayTeamLogo:matchData.awayTeamLogo,homeScore:matchData.homeScore||0,awayScore:matchData.awayScore||0,status:matchData.status,minute:matchData.minute||0,league:matchData.league,leagueLogo:matchData.leagueLogo,streamingType:change.templateType||"AWS-IVS",matchUrl:matchData.matchUrl,isLive:!!change.isLive,isStreamerLiveNow:!!change.isLive,hasStreamerTask:!0,streamerId:matchData.streamerId||change.streamerId,streamerNickname:matchData.streamerNickname,streamerCardBackground:matchData.streamerCardBackground,matchDate:matchData.matchDate}),"function"==typeof renderMatchSelectorCards)){const modal=document.getElementById("matchSelectorModal");modal&&modal.classList.contains("show")&&renderMatchSelectorCards()}}catch(error){console.error("[NewLiveStream] 添加卡片失败:",error)}}function escapeHtmlForLive(text){return null==text?"":String(text).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function formatLiveCardTime(matchDateStr){if(!matchDateStr)return"--:--";try{const date=new Date(matchDateStr);return isNaN(date.getTime())?"--:--":date.toLocaleString("vi-VN",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit",hour12:!1,timeZone:"Asia/Ho_Chi_Minh"}).replace(",","")}catch(e){return"--:--"}}function handleNewBeautyLiveAdded(change){const liveCardsContainer=document.getElementById("liveMatchCards");if(!liveCardsContainer)return;liveCardsContainer.classList.remove("d-none"),liveCardsContainer.closest(".focus-live-cards")?.classList.remove("d-none");const identifier=change.playbackIdentifier||change.identifier;if(document.querySelector(`.beauty-card[data-identifier="${identifier}"]`))return void updateBeautyLiveBadge(change);const cardHtml=createBeautyCardHtml(change);liveCardsContainer.insertAdjacentHTML("beforeend",cardHtml),"function"==typeof window.sortFocusCardsByTime&&window.sortFocusCardsByTime(liveCardsContainer),"function"==typeof window.applyBlvToFocusCardStreamers&&window.applyBlvToFocusCardStreamers(liveCardsContainer);const newCard=liveCardsContainer.querySelector(`.beauty-card[data-identifier="${identifier}"]`);newCard&&(newCard.style.opacity="0",newCard.style.transform="scale(0.95)",setTimeout(()=>{newCard.style.transition="opacity 0.3s, transform 0.3s",newCard.style.opacity="1",newCard.style.transform="scale(1)"},50))}
1function handleBeautyLiveRemoved(change){const identifier=change.playbackIdentifier||change.identifier,card=document.querySelector(`.beauty-card[data-identifier="${identifier}"]`);if(!card)return;card.classList.remove("is-live"),card.dataset.status="stopped";const badgeContainer=card.querySelector(".match-live-badge-container");badgeContainer&&(badgeContainer.innerHTML='\n            <div class="focus-card-badge">\n                <span class="badge bg-secondary">直播结束</span>\n            </div>\n        ')}function updateBeautyLiveBadge(change){const identifier=change.playbackIdentifier||change.identifier,card=document.querySelector(`.beauty-card[data-identifier="${identifier}"]`);if(!card)return void handleNewBeautyLiveAdded(change);card.dataset.status=change.isLive?"running":"pending",card.classList.toggle("is-live",!!change.isLive);const cardItem=card.closest(".focus-card-item");cardItem&&(cardItem.dataset.isLive=change.isLive?"true":"false");const badgeContainer=card.querySelector(".match-live-badge-container");if(!badgeContainer)return;const timeStr=formatLiveCardTime(change.matchDate);change.isLive?(badgeContainer.innerHTML='\n            <div class="focus-card-badge">\n                <span class="badge bg-danger beauty-live-badge"><span class="beauty-live-dot"></span>LIVE</span>\n            </div>\n        ',card.classList.contains("active")&&"function"==typeof window.retryFocusPlayback&&(window.PcPlaybackVisibility?.deferAutoPlay()||window.retryFocusPlayback())):badgeContainer.innerHTML=`\n            <div class="focus-card-badge focus-card-badge-right">\n                <span class="badge focus-time-badge">${timeStr}</span>\n            </div>\n        `,"function"==typeof window.sortFocusCardsByTime&&window.sortFocusCardsByTime()}function createBeautyCardHtml(change){const identifier=change.playbackIdentifier||change.identifier,title=escapeHtmlForLive(change.title||""),streamerRaw=change.streamerNickname||"",streamer=escapeHtmlForLive("function"==typeof formatBlvStreamerName?formatBlvStreamerName(streamerRaw):streamerRaw),streamerAttr=escapeHtmlForLive(streamerRaw),avatar=escapeHtmlForLive(change.streamerAvatar||""),isLive=!!change.isLive,streamingType=change.templateType||"AWS-IVS",timeStr=formatLiveCardTime(change.matchDate),bgStyle=avatar?`background-image:url(${avatar});`:"",liveBadge=isLive?'<div class="focus-card-badge"><span class="badge bg-danger beauty-live-badge"><span class="beauty-live-dot"></span>LIVE</span></div>':`<div class="focus-card-badge focus-card-badge-right"><span class="badge focus-time-badge">${timeStr}</span></div>`,sortTime=change.matchDate||"",isFocusPinned=!!change.isFocusPinned,isLiveJumpPinned=!!change.isLiveJumpPinned,focusPinWeight=Number(change.focusPinWeight)||0,streamerId=change.streamerId||"",followRank=window.StreamerFollow?.computeFollowRank?window.StreamerFollow.computeFollowRank(streamerId,isLive):isLive?1:3,isFollowed=!!window.StreamerFollow?.isFollowing&&window.StreamerFollow.isFollowing(streamerId),followHeart=streamerId&&window.StreamerFollow?.followHeartHtml?window.StreamerFollow.followHeartHtml():"";return`\n        <div class="focus-card-item${isFollowed?" is-followed":""}" data-sort-time="${sortTime}" data-is-live="${isLive}" data-sort-tier="1" data-follow-rank="${followRank}" data-is-focus-pinned="${isFocusPinned}" data-is-live-jump-pinned="${isLiveJumpPinned}" data-focus-pin-weight="${focusPinWeight}">\n            <div class="focus-card beauty-card${isLive?" is-live":""}"\n                 data-beauty-id="${change.streamSessionId}"\n                 data-identifier="${identifier}"\n                 data-streaming-type="${streamingType}"\n                 data-title="${title}"\n                 data-streamer="${streamerAttr}"\n                 data-streamer-slug="${escapeHtmlForLive(change.streamerSlug||"")}"\n                 data-streamer-id="${streamerId}"\n                 data-streamer-avatar="${avatar}"\n                 data-status="${isLive?"running":"pending"}"\n                 role="button" tabindex="0"\n                 onclick="window.switchFocusBeauty(this)">\n                <div class="focus-card-bg beauty-card-bg" style="${bgStyle}">\n                    ${followHeart}\n                    <div class="beauty-card-tag">\n                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="22"></line></svg>
1\n                        <span>Beauty</span>\n                    </div>\n                    <div class="match-live-badge-container">${liveBadge}</div>\n                    <div class="beauty-card-play">\n                        <svg width="40" height="40" viewBox="0 0 24 24" fill="rgba(255,255,255,0.95)"><circle cx="12" cy="12" r="11" fill="rgba(0,0,0,0.35)"></circle><polygon points="10 8 16 12 10 16" fill="#fff"></polygon></svg>\n                    </div>\n                    <div class="beauty-card-info">\n                        <div class="beauty-card-title">${title}</div>\n                    </div>\n                </div>\n            </div>\n            ${streamer?streamerId?`<a class="focus-card-streamer" href="/streamer/${streamerId}" onclick="event.stopPropagation();"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="me-1"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path></svg>${streamer}</a>`:`<div class="focus-card-streamer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="me-1"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path></svg>${streamer}</div>`:""}\n        </div>\n    `}function createFocusCardHtml(match,change){const cardId=change.identifier||`${match.id}_${change.streamSessionId||""}`,streamingType=change.templateType||"AWS-IVS",isLive=!1!==change.isLive&&("LIVE"===match.status||change.isLive),isManualFootball=!(!change.isManualFootball&&!match.isManualFootball),playbackId=change.playbackIdentifier||cardId,timeStr=formatLiveCardTime(match.matchDate||change.matchDate),hasStreamerBg=match.streamerCardBackground?`background-image: url(${match.streamerCardBackground});`:"",homeTeam=escapeHtmlForLive(match.homeTeam||""),awayTeam=escapeHtmlForLive(match.awayTeam||""),homeLogo=escapeHtmlForLive(match.homeTeamLogo||""),awayLogo=escapeHtmlForLive(match.awayTeamLogo||""),matchDateIso=match.matchDate||change.matchDate||"",isFocusPinned=!!(change.isFocusPinned??match.isFocusPinned),isLiveJumpPinned=!!(change.isLiveJumpPinned??match.isLiveJumpPinned),focusPinWeight=Number(change.focusPinWeight??match.focusPinWeight)||0,sortTier=!1===change.hasStreamerTask?2:0,streamerId=match.streamerId||change.streamerId||"",streamerNickname=match.streamerNickname||change.streamerNickname||"",followRank=window.StreamerFollow?.computeFollowRank?window.StreamerFollow.computeFollowRank(streamerId,isLive):isLive?1:3,isFollowed=!!window.StreamerFollow?.isFollowing&&window.StreamerFollow.isFollowing(streamerId),followHeart=streamerId&&window.StreamerFollow?.followHeartHtml?window.StreamerFollow.followHeartHtml():"",isWorldCup="function"==typeof window.isFocusWorldCupMatch&&window.isFocusWorldCupMatch(match.leagueApiId??change.leagueApiId,match.league??change.league),badgeHtml=isLive?`<div class="focus-card-badge">${"function"==typeof window.buildFocusLiveBadgeLabel?window.buildFocusLiveBadgeLabel(isWorldCup,"LIVE"):'<span class="badge bg-danger">LIVE</span>'}</div>\n           <div class="focus-card-badge focus-card-badge-right"><span class="badge focus-time-badge">${timeStr}</span></div>`:`<div class="focus-card-badge focus-card-badge-right"><span class="badge focus-time-badge">${timeStr}</span></div>`;return`\n        <div class="focus-card-item${isFollowed?" is-followed":""}" data-sort-time="${matchDateIso}" data-is-live="${isLive}" data-sort-tier="${sortTier}" data-follow-rank="${followRank}" data-is-focus-pinned="${isFocusPinned}" data-is-live-jump-pinned="${isLiveJumpPinned}" data-focus-pin-weight="${focusPinWeight}" data-is-world-cup="${isWorldCup}">\n            <div class="focus-card${isLive?" is-live":""}${isManualFootball?" manual-football-card":""}"\n                 data-card-id="${cardId}"\n                 data-identifier="${playbackId}"\n                 data-manual-football="${isManualFootball}"\n                 data-match-id="${match.id}"\n                 data-stream-session-id="${change.streamSessionId||""}"\n                 data-streaming-type="${streamingType}"\n                 data-home-team="${homeTeam}"\n                 data-away-team="${awayTeam}
1"\n                 data-home-logo="${homeLogo}"\n                 data-away-logo="${awayLogo}"\n                 data-home-score="${match.homeScore||0}"\n                 data-away-score="${match.awayScore||0}"\n                 data-status="${match.status}"\n                 data-minute="${match.minute||0}"\n                 data-league-logo="${escapeHtmlForLive(match.leagueLogo||"")}"\n                 data-match-url="${escapeHtmlForLive(match.matchUrl||"#")}"\n                 data-has-streamer-task="true"\n                 data-streamer="${escapeHtmlForLive(streamerNickname)}"\n                 data-streamer-id="${streamerId}"\n                 data-streamer-avatar="${escapeHtmlForLive(match.streamerCardBackground||"")}"\n                 data-match-date="${matchDateIso}"\n                 onclick="switchFocusMatch(${match.id}, '${change.streamSessionId||""}')">\n                <div class="focus-card-bg ${hasStreamerBg?"has-streamer-bg":""}" style="${hasStreamerBg}">\n                    ${followHeart}\n                    <div id="match_live_badge_${cardId}" class="match-live-badge-container">\n                        ${badgeHtml}\n                    </div>\n                </div>\n                <div class="focus-card-teams">\n                    <div class="focus-card-team-row">\n                        <img src="${homeLogo}" alt="${homeTeam}" loading="lazy" onerror="this.onerror=null; this.src='/images/placeholder-team.svg';">\n                        <span class="focus-card-team-name">${homeTeam}</span>\n                        ${isManualFootball?"":`<span class="focus-card-score home-score" data-match-id="${match.id}">${match.homeScore||0}</span>`}\n                    </div>\n                    <div class="focus-card-team-row">\n                        <img src="${awayLogo}" alt="${awayTeam}" loading="lazy" onerror="this.onerror=null; this.src='/images/placeholder-team.svg';">\n                        <span class="focus-card-team-name">${awayTeam}</span>\n                        ${isManualFootball?"":`<span class="focus-card-score away-score" data-match-id="${match.id}">${match.awayScore||0}</span>`}\n                    </div>\n                </div>\n            </div>\n            ${streamerNickname?streamerId?`<a class="focus-card-streamer" href="/streamer/${streamerId}" onclick="event.stopPropagation();"><i class="fas fa-microphone-alt me-1"></i>${escapeHtmlForLive("function"==typeof formatBlvStreamerName?formatBlvStreamerName(streamerNickname):streamerNickname)}</a>`:`<div class="focus-card-streamer"><i class="fas fa-microphone-alt me-1"></i>${escapeHtmlForLive("function"==typeof formatBlvStreamerName?formatBlvStreamerName(streamerNickname):streamerNickname)}</div>`:""}\n        </div>\n    `}function tryAutoPlayOnLiveStatusChange(changes){if(!changes||0===changes.length)return;if(window.PcPlaybackVisibility?.deferAutoPlay())return;const focusPlayerContainer=document.getElementById("focusPlayerContainer");if(!focusPlayerContainer)return;if(!changes.some(change=>!0===change.isLive))return;const isWaitingState=focusPlayerContainer.classList.contains("focus-no-stream")||!!focusPlayerContainer.querySelector(".focus-not-live-overlay"),videoElement=document.getElementById("focusLivePlayer"),isAlreadyPlaying=videoElement&&!videoElement.paused&&videoElement.readyState>=2&&videoElement.videoWidth>0;if(!isWaitingState&&isAlreadyPlaying)return;if("function"==typeof window.tryAutoPlayFirstLiveFocus)return void setTimeout(()=>{window.tryAutoPlayFirstLiveFocus()},800);let currentMatchId=focusPlayerContainer.getAttribute("data-match-id");if(!currentMatchId){const activeCard=document.querySelector(".focus-card.active");activeCard&&(currentMatchId=activeCard.getAttribute("data-match-id"))}if(!currentMatchId)return;currentMatchId=parseInt(currentMatchId,10);const currentMatchChange=changes.find(change=>parseInt(change.matchId,10)===currentMatchId&&!0===change.isLive);currentMatchChange&&setTimeout(()=>{if("function"==typeof window.switchFocusMatch){const sessionId=currentMatchChange.streamSessionId;window.switchFocusMatch(currentMatchId,sessionId)}},800)}function initUserMenu(){const userMenuToggle=document.getElementById("userMenuToggle"),userMenuContainer=document.getElementById("userMenuContainer");userMenuToggle&&userMenuContainer&&(userMenuToggle.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),userMenuContainer.classList.toggle("show")}),document.addEventListener("click",function(e){const isClickInsideMenu=userMenuContainer.contains(e.target),isMenuButton=e.target===userMenuToggle||userMenuToggle.contains(e.target);isClickInsideMenu||isMenuButton||userMenuContainer.classList.remove("show")}))}function initMobileUserMenu(){const mobileUserMenuToggle=document.getElementById("mobileUserMenuToggle"),mobileUserMenuContainer=document.getElementById("mobileUserMenuContainer");mobileUserMenuToggle&&mobileUserMenuContainer&&(mobileUserMenuToggle.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),mobileUserMenuContainer.classList.toggle("show")}),document.addEventListener("click",function(e){const isClickInsideMenu=mobileUserMenuContainer.contains(e.target),isMenuButton=e.target===mobileUserMenuToggle||mobileUserMenuToggle.contains(e.target);isClickInsideMenu||isMenuButton||mobileUserMenuContainer.classList.remove("show")}))}window.MatchLiveHub=function(){let connection=null,currentMatchId=null,isConnected=!1,reconnectAttempts=0,silentRefreshTimer=null,pendingRefreshCount=0;const domCache={playerHeaderStatus:null,statusBadge:null,eventsContainers:null,matchCardsCache:new Map,focusCardsCache:new Map,stripItemsCache:new Map,minuteElsCache:new Map,liveCountGroups:null,lastCacheTime:0,CACHE_TTL:5e3,LIVE_COUNT_TTL:3e4,liveCountCachedAt:0,get(key){const now=Date.now();if(now-this.lastCacheTime>this.CACHE_TTL&&this.clear(),!this[key]){switch(key){case"playerHeaderStatus":this[key]=document.querySelector(".live-player
1-header .focus-status, .focus-player-header .focus-status");break;case"statusBadge":this[key]=document.querySelector(".match-status-badge");break;case"eventsContainers":this[key]=document.querySelectorAll(".events-timeline:not(#panel-textLive .events-timeline)")}this.lastCacheTime=now}return this[key]},getMatchCards(matchId){const now=Date.now();if(now-this.lastCacheTime>this.CACHE_TTL&&this.matchCardsCache.clear(),!this.matchCardsCache.has(matchId)){const cards=document.querySelectorAll(`.match-card[href*="${matchId}"], [data-match-id="${matchId}"]`);this.matchCardsCache.set(matchId,cards),this.lastCacheTime=now}return this.matchCardsCache.get(matchId)},getFocusCards(matchId){const now=Date.now();if(now-this.lastCacheTime>this.CACHE_TTL&&this.focusCardsCache.clear(),!this.focusCardsCache.has(matchId)){const cards=document.querySelectorAll(`.focus-card[data-match-id="${matchId}"], .focus-match-card[data-match-id="${matchId}"]`);this.focusCardsCache.set(matchId,cards),this.lastCacheTime=now}return this.focusCardsCache.get(matchId)},getStripItems(matchId){const now=Date.now();if(now-this.lastCacheTime>this.CACHE_TTL&&this.stripItemsCache.clear(),!this.stripItemsCache.has(matchId)){const items=document.querySelectorAll(`.strip-item[href*="${matchId}"]`);this.stripItemsCache.set(matchId,items),this.lastCacheTime=now}return this.stripItemsCache.get(matchId)},getMinuteEls(matchId){const now=Date.now();if(now-this.lastCacheTime>this.CACHE_TTL&&this.minuteElsCache.clear(),!this.minuteElsCache.has(matchId)){const els=document.querySelectorAll(`.js-match-minute[data-match-id="${matchId}"]`);this.minuteElsCache.set(matchId,els),this.lastCacheTime=now}return this.minuteElsCache.get(matchId)},getLiveCountGroups(){const now=Date.now();return(!this.liveCountGroups||now-this.liveCountCachedAt>this.LIVE_COUNT_TTL)&&(this.liveCountGroups={statLiveItems:document.querySelectorAll(".stat-item.live"),liveBadges:document.querySelectorAll(".badge.bg-danger"),filterTabBadges:document.querySelectorAll(".filter-tab .badge"),sidebarLiveCounts:document.querySelectorAll(".fs-4.fw-bold.text-danger"),otherLiveCounts:document.querySelectorAll("[data-live-count], .live-count-badge")},this.liveCountCachedAt=now),this.liveCountGroups},clear(){this.playerHeaderStatus=null,this.statusBadge=null,this.eventsContainers=null,this.matchCardsCache.clear(),this.focusCardsCache.clear(),this.stripItemsCache.clear(),this.minuteElsCache.clear(),this.liveCountGroups=null,this.liveCountCachedAt=0,this.lastCacheTime=0}},MinuteTicker=function(){function updateOne(el){if(!el)return;const minute=el.dataset.minute;if(void 0!==minute&&""!==minute){const displayText=`${minute}'`;el.textContent!==displayText&&(el.textContent=displayText)}}let detailMinuteEls=null,detailMinuteMatchId=null;function setMinute(matchId,minute){domCache.getMinuteEls(matchId).forEach(el=>{el.dataset.minute=minute,updateOne(el)}),window.MatchLiveHub&&window.MatchLiveHub.getCurrentMatchId()==matchId&&(detailMinuteEls&&detailMinuteMatchId===matchId||(detailMinuteEls=document.querySelectorAll(".js-match-minute"),detailMinuteMatchId=matchId),detailMinuteEls.forEach(el=>{el.dataset.matchId||(el.dataset.minute=minute,updateOne(el))}))}return{start:function start(){},stop:function stop(){},syncStatus:function syncStatus(el,status){el&&(el.dataset.status=status,updateOne(el))},freeze:function freeze(el){el&&(el.dataset.freeze="1")},setMinute:setMinute,setAllMinutes:function setAllMinutes(minutesMap){for(const[matchId,minute]of Object.entries(minutesMap))setMinute(matchId,minute)}}}(),callbacks={onMatchStarted:[],onMatchUpdate:[],onMatchEnded:[],onConnected:[],onDisconnected:[]};async function startConnection(){try{await connection.start(),isConnected=!0,reconnectAttempts=0,showConnectionStatus("connected");try{await connection.invoke("SubscribeToListPage","list")}catch(err){console.error("❌ 订阅直播状态更新失败:",err)}window.MatchStore&&!window.MatchStore.isInitialized()&&(await window.MatchStore.init("live"),window.MatchStore.isInitialized()&&window.MatchPollingService&&window.MatchPollingService.stopPolling()),callbacks.onConnected.forEach(cb=>cb())}catch(err){console.error("❌ WebSocket 连接失败:",err),isConnected=!1,showConnectionStatus("error"),reconnectAttempts<5&&setTimeout(()=>
1{reconnectAttempts++,startConnection()},3e3)}}async function joinMatchRoom(matchId){if(!connection||!isConnected)return console.warn("WebSocket 未连接,无法加入房间"),!1;try{return await connection.invoke("JoinMatchRoom",parseInt(matchId)),currentMatchId=matchId,!0}catch(err){return console.error("加入比赛房间失败:",err),!1}}let lastVersions={},lastBatchVersion=0,isFetching=!1;async function fetchAndUpdateMatchDetail(matchId){if(!isFetching){isFetching=!0;try{const response=await fetch(`/api/fixtures/${matchId}/live`);if(!response.ok)return void console.error("拉取比赛详情失败:",response.status);const data=await response.json();if(data&&window.MatchStore&&window.MatchStore.isInitialized()){const patch=function convertDataToPatch(data){return{"status.short":data.status,minute:data.minute,"score.home":data.homeScore,"score.away":data.awayScore,"stats.home.possession":data.homePossession,"stats.away.possession":data.awayPossession,"stats.home.shots":data.homeShots,"stats.away.shots":data.awayShots,"stats.home.shotsOnTarget":data.homeShotsOnTarget,"stats.away.shotsOnTarget":data.awayShotsOnTarget,"stats.home.corners":data.homeCorners,"stats.away.corners":data.awayCorners,"stats.home.fouls":data.homeFouls,"stats.away.fouls":data.awayFouls,"stats.home.yellowCards":data.homeYellowCards,"stats.away.yellowCards":data.awayYellowCards,"stats.home.redCards":data.homeRedCards,"stats.away.redCards":data.awayRedCards,"stats.home.offsides":data.homeOffsides,"stats.away.offsides":data.awayOffsides,isLive:data.isLive,events:data.events}}(data);return void window.MatchStore.applyPatch(matchId,Date.now(),patch,null)}data&&(updateScoreDisplay(matchId,data),updateMatchInfo(matchId,data),updateStatsDisplay(matchId,data),updateEventsDisplay(matchId,data),updateMatchStatus(matchId,data))}catch(err){console.error("拉取比赛详情出错:",err)}finally{isFetching=!1}}}function updateScoreDisplay(matchId,payload){if(parseInt(matchId,10)<0)return;if(!payload||"object"!=typeof payload)return void console.warn("无效的 payload:",payload);const heroScore=document.querySelector(".match-detail-hero .score-display");if(heroScore&&isCurrentMatch(matchId)){const homeScore=heroScore.querySelector(".home-score"),awayScore=heroScore.querySelector(".away-score");homeScore&&void 0!==payload.homeScore&&null!==payload.homeScore&&animateScoreChange(homeScore,payload.homeScore),awayScore&&void 0!==payload.awayScore&&null!==payload.awayScore&&animateScoreChange(awayScore,payload.awayScore)}const playerHeaderScore=document.querySelector(".live-player-header .focus-score-display, .focus-player-header .focus-score-display");if(playerHeaderScore&&isCurrentMatch(matchId)){const homeScore=playerHeaderScore.querySelector(".home-score"),awayScore=playerHeaderScore.querySelector(".away-score");homeScore&&void 0!==payload.homeScore&&null!==payload.homeScore&&animateScoreChange(homeScore,payload.homeScore),awayScore&&void 0!==payload.awayScore&&null!==payload.awayScore&&animateScoreChange(awayScore,payload.awayScore)}domCache.getMatchCards(matchId).forEach(matchCard=>{const scoreHome=matchCard.querySelector(".score-home"),scoreAway=matchCard.querySelector(".score-away");scoreHome&&void 0!==payload.homeScore&&null!==payload.homeScore&&animateScoreChange(scoreHome,payload.homeScore),scoreAway&&void 0!==payload.awayScore&&null!==payload.awayScore&&animateScoreChange(scoreAway,payload.awayScore)});const focusScore=document.querySelector(".focus-score .score-big");if(focusScore&&isCurrentMatch(matchId)){const spans=focusScore.querySelectorAll("span:not(.sep)");spans.length>=2&&(void 0!==payload.homeScore&&null!==payload.homeScore&&animateScoreChange(spans[0],payload.homeScore),void 0!==payload.awayScore&&null!==payload.awayScore&&animateScoreChange(spans[1],payload.awayScore))}domCache.getFocusCards(matchId).forEach(focusCard=>{const cardHomeScore=focusCard.querySelector(".focus-card-score.home-score")||focusCard.querySelector(".card-score-display .home-score"),cardAwayScore=focusCard.querySelector(".focus-card-score.away-score")||focusCard.querySelector(".card-score-display .away-score");cardHomeScore&&void 0!==payload.homeScore&&null!==payload.homeScore&&animateScoreChange(cardHomeScore,payload.homeScore),cardAwayScore&&void 0!==payload.awayScore&&null!==payload.awayScore&&animateScoreChange(cardAwayScore,payload.awayScore);const vsEl=focusCard.querySelector(".focus-card-vs");vsEl&&void 0!==payload.homeScore&&void 0!==payload.awayScore&&(vsEl.textContent=`${payload.homeScore} - ${payload.awayScore}`)}
1),domCache.getStripItems(matchId).forEach(stripItem=>{const stripTeams=stripItem.querySelector(".strip-teams");stripTeams&&void 0!==payload.homeScore&&void 0!==payload.awayScore&&payload.homeTeam&&payload.awayTeam&&(stripTeams.textContent=`${payload.homeTeam} ${payload.homeScore} - ${payload.awayScore} ${payload.awayTeam}`)}),function updateBasketballQuarterScores(matchId,payload){if(void 0!==payload.quarter1HomeScore||void 0!==payload.quarter1AwayScore||void 0!==payload.quarter2HomeScore||void 0!==payload.quarter2AwayScore||void 0!==payload.quarter3HomeScore||void 0!==payload.quarter3AwayScore||void 0!==payload.quarter4HomeScore||void 0!==payload.quarter4AwayScore){if(isCurrentMatch(matchId)){const homeRow=document.querySelector(".quarters-row.home-row"),awayRow=document.querySelector(".quarters-row.away-row");if(homeRow&&awayRow){const homeCells=homeRow.querySelectorAll(".quarter-cell:not(.team-cell):not(.total-cell)"),awayCells=awayRow.querySelectorAll(".quarter-cell:not(.team-cell):not(.total-cell)");[{home:payload.quarter1HomeScore,away:payload.quarter1AwayScore,index:0},{home:payload.quarter2HomeScore,away:payload.quarter2AwayScore,index:1},{home:payload.quarter3HomeScore,away:payload.quarter3AwayScore,index:2},{home:payload.quarter4HomeScore,away:payload.quarter4AwayScore,index:3}].forEach(q=>{void 0!==q.home&&homeCells[q.index]&&animateScoreChange(homeCells[q.index],q.home),void 0!==q.away&&awayCells[q.index]&&animateScoreChange(awayCells[q.index],q.away)})}}domCache.getMatchCards(matchId).forEach(matchCard=>{const quarterScores=matchCard.querySelector(".quarter-scores");if(quarterScores){const quarterSpans=quarterScores.querySelectorAll(".quarter-score");[{home:payload.quarter1HomeScore,away:payload.quarter1AwayScore,index:0,label:"Q1"},{home:payload.quarter2HomeScore,away:payload.quarter2AwayScore,index:1,label:"Q2"},{home:payload.quarter3HomeScore,away:payload.quarter3AwayScore,index:2,label:"Q3"},{home:payload.quarter4HomeScore,away:payload.quarter4AwayScore,index:3,label:"Q4"}].forEach(u=>{if((void 0!==u.home||void 0!==u.away)&&quarterSpans[u.index]){const homeScore=void 0!==u.home?u.home:parseInt(quarterSpans[u.index].textContent.split(":")[1]?.split("-")[0]||"0"),awayScore=void 0!==u.away?u.away:parseInt(quarterSpans[u.index].textContent.split("-")[1]||"0");quarterSpans[u.index].textContent=`${u.label}: ${homeScore}-${awayScore}`}})}})}}(matchId,payload)}function animateScoreChange(element,newScore){const currentScore=element.textContent.trim();currentScore!==String(newScore)&&requestAnimationFrame(()=>{element.classList.add("score-updating"),element.textContent=newScore,parseInt(newScore)>parseInt(currentScore||0)&&(element.classList.add("goal-scored"),setTimeout(()=>{requestAnimationFrame(()=>{element.classList.remove("goal-scored")})},3e3)),setTimeout(()=>{requestAnimationFrame(()=>{element.classList.remove("score-updating")})},500)})}function updateStatsDisplay(matchId,payload){isCurrentMatch(matchId)&&(void 0!==payload.homePossession&&(updateStatBar("possession",payload.homePossession,payload.awayPossession),function updatePossessionBar(homeValue,awayValue){const homeFill=document.querySelector(".stat-bar-fill.home"),awayFill=document.querySelector(".stat-bar-fill.away"),homeText=document.querySelector(".stat-bar-value.home"),awayText=document.querySelector(".stat-bar-value.away");homeFill&&(homeFill.style.width=`${homeValue}%`),awayFill&&(awayFill.style.width=`${awayValue}%`),homeText&&(homeText.textContent=`${homeValue}%`),awayText&&(awayText.textContent=`${awayValue}%`)}(payload.homePossession,payload.awayPossession)),void 0!==payload.homeShots&&updateStatValue("shots",payload.homeShots,payload.awayShots),void 0!==payload.homeShotsOnTarget&&updateStatValue("shotsOnTarget",payload.homeShotsOnTarget,payload.awayShotsOnTarget),void 0!==payload.homeCorners&&updateStatValue("corners",payload.homeCorners,payload.awayCorners),void 0!==payload.homeFouls&&updateStatValue("fouls",payload.homeFouls,payload.awayFouls),void 0!==payload.homeYellowCards&&updateStatValue("yellowCards",payload.homeYellowCards,payload.awayYellowCards),void 0!==payload.homeRedCards&&updateStatValue("redCards",payload.homeRedCards,payload.awayRedCards),void 0!==payload.homeOff
1sides&&updateStatValue("offsides",payload.homeOffsides,payload.awayOffsides),void 0!==payload.homePasses&&updateStatValue("passes",payload.homePasses,payload.awayPasses),void 0!==payload.homePassAccuracy&&updateStatBar("passAccuracy",payload.homePassAccuracy,payload.awayPassAccuracy))}function updateTextLiveDisplay(matchId,payload){if(!isCurrentMatch(matchId)||!payload.events)return;let textLiveContainer=document.querySelector("#panel-textLive .events-timeline");if(!textLiveContainer){const panel=document.querySelector("#panel-textLive");if(!panel)return;const emptyPanel=panel.querySelector(".empty-panel");emptyPanel&&emptyPanel.remove(),textLiveContainer=document.createElement("div"),textLiveContainer.className="events-timeline",panel.appendChild(textLiveContainer)}const eventsHtml=sortEventsNewestFirst(payload.events).map(event=>createEventHtml(event)).join("");textLiveContainer.innerHTML=eventsHtml;const newEvent=textLiveContainer.firstElementChild;newEvent&&(newEvent.classList.add("new-event"),setTimeout(()=>newEvent.classList.remove("new-event"),3e3))}function sortEventsNewestFirst(events){if(!events||!Array.isArray(events))return[];const getMinute=e=>e.minute??e.Minute??e.elapsed??e.Elapsed??e.time?.elapsed??e.time?.Elapsed??0,getSeq=e=>e?.seq??e?._seq??e?.Seq??0;return events.map((e,i)=>({e:e,i:i})).sort((a,b)=>{const diff=getMinute(b.e)-getMinute(a.e);if(0!==diff)return diff;const seqDiff=getSeq(b.e)-getSeq(a.e);return 0!==seqDiff?seqDiff:b.i-a.i}).map(x=>x.e)}function updateStatBar(statName,homeValue,awayValue){const statRow=document.querySelector(`[data-stat="${statName}"]`);if(!statRow)return;const homeBar=statRow.querySelector(".stat-bar-home")||statRow.querySelector(".stat-bar-fill.home"),awayBar=statRow.querySelector(".stat-bar-away")||statRow.querySelector(".stat-bar-fill.away"),homeText=statRow.querySelector(".stat-home")||statRow.querySelector(".stat-bar-value.home"),awayText=statRow.querySelector(".stat-away")||statRow.querySelector(".stat-bar-value.away");homeBar&&(homeBar.style.width=`${homeValue}%`),awayBar&&(awayBar.style.width=`${awayValue}%`),homeText&&(homeText.textContent=`${homeValue}%`),awayText&&(awayText.textContent=`${awayValue}%`)}function updateStatValue(statName,homeValue,awayValue){const statRow=document.querySelector(`[data-stat="${statName}"]`);if(!statRow)return;const homeText=statRow.querySelector(".stat-home .stat-value"),awayText=statRow.querySelector(".stat-away .stat-value");homeText&&(homeText.textContent=homeValue),awayText&&(awayText.textContent=awayValue)}function updateEventsDisplay(matchId,payload){if(!payload.events||!Array.isArray(payload.events))return;const eventsHtml=sortEventsNewestFirst(payload.events).map(event=>createEventHtml(event)).join(""),containers=domCache.get("eventsContainers");containers&&containers.length>0&&containers.forEach(container=>{container.innerHTML=eventsHtml;const newEvent=container.firstElementChild;newEvent&&(newEvent.classList.add("new-event"),setTimeout(()=>newEvent.classList.remove("new-event"),3e3))}),updateTextLiveDisplay(matchId,payload)}function createEventHtml(rawEvent){const event=function normalizeEvent(event){if(!event)return null;let minute=event.minute??event.Minute??0;0===minute&&event.time&&(minute=event.time.elapsed??event.time.Elapsed??0);let team=event.team??event.Team??"";if("object"==typeof team&&null!==team){const teamName=team.name??team.Name??"",homeTeamEl=document.querySelector('.match-header .team-info:first-child .team-name-display, .team-name.home, [data-team="home"] .team-name, #homeTeam, .pitch-half.home .pitch-team-name'),homeTeamName=homeTeamEl?.textContent?.trim()||window._homeTeamName||"";team=teamName&&teamName===homeTeamName?"Home":"Away"}let player=event.player??event.Player??"";return"object"==typeof player&&null!==player&&(player=player.name??player.Name??""),{minute:minute,type:event.type??event.Type??"",team:team,player:player,detail:event.detail??event.Detail??""}}(rawEvent);if(!event)return"";const teamClass="Home"===event.team?"event-home":"event-away",eventType=event.type||"",eventDetail=(event.detail||"").toLowerCase(),isGoal="Goal"===eventType,isYellow="Yellow"===eventType||"Card"===eventType&&eventDetail.includes("yellow"),isRed="Red"===eventType||"Card"===eventType&&(eventDetail.includes("red")||eventDetail.includes("second yellow")),isVar="Var"===eventType,iconClass=isGoal?"goal":isYellow?"yellow":isRed?"red":isVar?"var":"sub",iconTitle=isGoal?"进球":isYellow?"黄牌":isRed?"红牌":isVar?"VAR":"换人",iconSvg=isGoal?'<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 2a10 10 0 100 20 10 10 0 000-20zm0 3l2.5 2-1 3h-3l-1-3L12 5zm-4.5 4l3 1v3l-2.5 2-2-2.5 1.5-3.5zm9 0l1.5 3.5-2 2.5-2.5-2v-3l3-1zM7 15.5l2-2.5h4l2 2.5-2 3h-4l-2-3z"/></svg>':isYellow?'<svg viewBox="0 0 24 24"><rect x="6" y="3" width="12" height="18" rx="2" fill="#fbbf24"/></svg>':isRed?'<svg viewBox="0 0 24 24"><rect x="6" y="3" width="12" height="18" rx="2" fill="#ef4444"/></svg>':isVar?'<svg viewBox="0 0 24 24" fill="currentColor"><rect x="2" y="4" width="20" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><text x="12" y="15" text-anchor="middle" font-size="8" font-weight="bold">VAR</text></svg>':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16l-4-4 4-4"/><path d="M17 8l4 4-4 4"/><line x1="3" y1="12" x2="21" y2="12"/></svg>';
1return"Home"===event.team?`\n                <div class="event-row ${teamClass}">\n                    <div class="event-left">\n                        <div class="event-content">\n                            <span class="event-player">${event.player}</span>\n                            <span class="event-detail">${event.detail}</span>\n                        </div>\n                        <div class="event-icon ${iconClass}" title="${iconTitle}">${iconSvg}</div>\n                    </div>\n                    <div class="event-time"><span>${event.minute}'</span></div>\n                    <div class="event-right"></div>\n                </div>\n            `:`\n                <div class="event-row ${teamClass}">\n                    <div class="event-left"></div>\n                    <div class="event-time"><span>${event.minute}'</span></div>\n                    <div class="event-right">\n                        <div class="event-icon ${iconClass}" title="${iconTitle}">${iconSvg}</div>\n                        <div class="event-content">\n                            <span class="event-player">${event.player}</span>\n                            <span class="event-detail">${event.detail}</span>\n                        </div>\n                    </div>\n                </div>\n            `}function updateMatchInfo(matchId,payload){if(!payload||"object"!=typeof payload)return void console.warn("无效的 payload:",payload);if(!isCurrentMatch(matchId))return;function normalizeLogoUrl(url){return url?"undefined"===url||"null"===url?"/images/placeholder-team.svg":url:"/images/placeholder-team.svg"}const teamsEl=document.querySelector(".live-player-header .focus-teams");if(teamsEl){const homeLogoImg=teamsEl.querySelector("img:first-child"),awayLogoImg=teamsEl.querySelector("img:last-child");homeLogoImg&&void 0!==payload.homeTeamLogo&&(homeLogoImg.src=normalizeLogoUrl(payload.homeTeamLogo),payload.homeTeam&&(homeLogoImg.alt=payload.homeTeam)),awayLogoImg&&void 0!==payload.awayTeamLogo&&(awayLogoImg.src=normalizeLogoUrl(payload.awayTeamLogo),payload.awayTeam&&(awayLogoImg.alt=payload.awayTeam))}const heroHomeLogo=document.querySelector(".match-detail-hero .team-logo.home"),heroAwayLogo=document.querySelector(".match-detail-hero .team-logo.away");heroHomeLogo&&void 0!==payload.homeTeamLogo&&(heroHomeLogo.src=normalizeLogoUrl(payload.homeTeamLogo)),heroAwayLogo&&void 0!==payload.awayTeamLogo&&(heroAwayLogo.src=normalizeLogoUrl(payload.awayTeamLogo))}function updateMatchStatus(matchId,payload){if(!payload||"object"!=typeof payload)return void console.warn("无效的 payload:",payload);if(void 0!==payload.minute&&null!==payload.minute&&MinuteTicker.setMinute(matchId,payload.minute),isCurrentMatch(matchId)){const playerHeaderStatus=domCache.get("playerHeaderStatus");if(playerHeaderStatus){const statusTextEl=playerHeaderStatus.querySelector(".status-text"),statusMinuteEl=playerHeaderStatus.querySelector(".status-minute");if(statusTextEl&&void 0!==payload.status&&null!==payload.status&&(statusTextEl.textContent=getStatusText(payload.status)),void 0!==payload.minute&&null!==payload.minute&&payload.minute>0)if(statusMinuteEl)statusMinuteEl.textContent=`${payload.minute}'`,statusMinuteEl.dataset.minute=payload.minute;else{const newMinuteEl=document.createElement("span");newMinuteEl.className="status-minute js-match-minute",newMinuteEl.dataset.matchId=matchId,newMinuteEl.dataset.minute=payload.minute,newMinuteEl.textContent=`${payload.minute}'`,playerHeaderStatus.appendChild(newMinuteEl)}}const statusBadge=domCache.get("statusBadge");if(statusBadge&&void 0!==payload.status&&null!==payload.status){const statusText=getStatusText(payload.status),isLive=["1H","2H","ET","HT","P"].includes(payload.status)||payload.isLive,isFinished=["FT","AET","PEN"].includes(payload.status),isHalftime="HT"===payload.status;if(isFinished)statusBadge.className="match-status-badge finished",statusBadge.innerHTML=statusText,statusBadge.querySelectorAll(".js-match-minute").forEach(MinuteTicker.freeze);else if(isHalftime)statusBadge.className="match-status-badge halft
1ime",statusBadge.innerHTML=`<span class="pulse-dot"></span><span>${statusText}</span>`,statusBadge.querySelectorAll(".js-match-minute").forEach(MinuteTicker.freeze);else if(isLive){statusBadge.className="match-status-badge live";const minute=payload.minute??0;statusBadge.innerHTML=minute>0?`<span class="pulse-dot"></span><span class="status-minute js-match-minute" data-match-id="${matchId}" data-minute="${minute}">${minute}'</span>`:`<span class="pulse-dot"></span><span>${statusText}</span>`;const minuteEl=statusBadge.querySelector(".status-minute");minuteEl&&(minuteEl.classList.add("pulse"),setTimeout(()=>minuteEl.classList.remove("pulse"),1e3))}else statusBadge.className="match-status-badge upcoming",statusBadge.innerHTML=statusText,statusBadge.querySelectorAll(".js-match-minute").forEach(MinuteTicker.freeze)}}const matchCards=domCache.getMatchCards(matchId);matchCards&&matchCards.length>0&&matchCards.forEach(matchCard=>{const timeEl=matchCard.querySelector(".match-time");timeEl&&void 0!==payload.minute&&(timeEl.dataset.minute=payload.minute,timeEl.dataset.matchId=matchId,timeEl.classList.contains("js-match-minute")&&(timeEl.textContent=`${payload.minute}'`));const statusEl=matchCard.querySelector(".match-status");if(statusEl&&payload.status)if(["FT","AET","PEN"].includes(payload.status))statusEl.textContent=getStatusText(payload.status),statusEl.className="match-status status-ft";else{let statusClass="match-status";switch(payload.status){case"1H":statusClass+=" status-1h";break;case"2H":statusClass+=" status-2h";break;case"HT":statusClass+=" status-ht";break;default:statusClass+=" status-live"}statusEl.className=statusClass,statusEl.innerHTML=`<span class="pulse-dot"></span><span>${getStatusText(payload.status)}</span>`}})}function updateMatchCardStatus(matchId,status,payload){const matchCards=domCache.getMatchCards(matchId);matchCards.length&&(matchCards.forEach(matchCard=>{if("live"===status){matchCard.classList.add("live"),matchCard.classList.remove("upcoming");const statusEl=matchCard.querySelector(".match-status");if(statusEl){let statusClass="match-status",statusText="LIVE";if(payload&&payload.status)switch(payload.status){case"1H":statusClass+=" status-1h",statusText=getStatusText("1H");break;case"2H":statusClass+=" status-2h",statusText=getStatusText("2H");break;case"HT":statusClass+=" status-ht",statusText=getStatusText("HT");break;default:statusClass+=" status-live"}else statusClass+=" status-live";statusEl.className=statusClass,statusEl.innerHTML=`<span class="pulse-dot"></span><span>${statusText}</span>`}}else if("finished"===status){matchCard.classList.remove("live"),matchCard.classList.add("finished");const statusEl=matchCard.querySelector(".match-status");statusEl&&(statusEl.textContent="undefined"!=typeof L&&L.T?L.T("match.status.finished"):"已结束",statusEl.className="match-status status-ft")}if(payload){const scoreHomeElements=matchCard.querySelectorAll(".score-home, .home-score"),scoreAwayElements=matchCard.querySelectorAll(".score-away, .away-score");scoreHomeElements.forEach(el=>{void 0!==payload.homeScore&&(el.textContent=payload.homeScore)}),scoreAwayElements.forEach(el=>{void 0!==payload.awayScore&&(el.textContent=payload.awayScore)}),void 0!==payload.homeScore&&(matchCard.dataset.homeScore=payload.homeScore),void 0!==payload.awayScore&&(matchCard.dataset.awayScore=payload.awayScore),payload.status&&(matchCard.dataset.status=payload.status),void 0!==payload.minute&&(matchCard.dataset.minute=payload.minute)}}),function updateFocusPlayerHeader(matchId,status,payload){const focusPlayerContainer=document.getElementById("focusPlayerContainer");if(!focusPlayerContainer||focusPlayerContainer.dataset.matchId!=matchId)return;if(!payload)return;const homeScoreEl=document.querySelector(".focus-player-header .home-score"),awayScoreEl=document.querySelector(".focus-player-header .away-score");homeScoreEl&&void 0!==payload.homeScore&&(homeScoreEl.textContent=payload.homeScore),awayScoreEl&&void 0!==payload.awayScore&&(awayScoreEl.textContent=payload.awayScore);const statusTextEl=document.querySelector(".focus-player-header .status-text");statusTextEl&&payload.status&&(statusTextEl.textContent=getStatusText(payload.status));const minuteEl=document.querySelector(".focus-player-header .status-minute");minuteEl&&void 0!==payload.minute&&(minuteEl.dataset.minute=payload.minute,minuteEl.dataset.matchI
1d=matchId,minuteEl.textContent=payload.minute>0?`${payload.minute}'`:"")}(matchId,0,payload))}function updateBroadcastStrip(matchId,payload,action){const stripScroll=document.querySelector(".strip-scroll");if(stripScroll&&"started"===action){const newItem=document.createElement("a");newItem.href=`${window.__matchRoutePrefix||"/football"}/${matchId}`,newItem.className="strip-item new-live",newItem.innerHTML=`\n                <span class="strip-live-dot"></span>\n                <span class="strip-teams">${payload.homeTeam} 0 - 0 ${payload.awayTeam}</span>\n                <span class="strip-time">0'</span>\n            `,stripScroll.insertBefore(newItem,stripScroll.firstChild);const noLive=stripScroll.querySelector(".no-live");noLive&&noLive.remove(),setTimeout(()=>newItem.classList.remove("new-live"),1e3)}}function setMatchNotificationMuted(muted){try{muted?localStorage.setItem("matchNotificationMuted","true"):localStorage.removeItem("matchNotificationMuted")}catch(e){console.warn("无法保存通知设置:",e)}}function showConnectionStatus(status){let statusEl=document.getElementById("wsConnectionStatus");if("connected"===status)return void(statusEl&&statusEl.remove());statusEl||(statusEl=document.createElement("div"),statusEl.id="wsConnectionStatus",statusEl.className="ws-connection-status",document.body.appendChild(statusEl));const t=(key,fallback)=>"undefined"!=typeof L&&L.T?L.T(key):fallback,statusInfo={reconnecting:{text:t("match.reconnecting","重新连接中..."),class:"warning",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ws-icon-spin"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>'},disconnected:{text:t("match.disconnected","连接已断开"),class:"danger",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'},error:{text:t("match.connectionFailed","连接失败"),class:"danger",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>'}}[status]||{text:t("match.unknownStatus","未知状态"),class:"secondary",icon:""};statusEl.innerHTML=(statusInfo.icon||"")+"<span>"+statusInfo.text+"</span>",statusEl.className=`ws-connection-status ws-status-${statusInfo.class}`}function isListPage(){const path=window.location.pathname.toLowerCase();return"/"===path||"/home"===path||"/home/index"===path||path.startsWith("/match")&&!path.includes("/details")}async function silentRefreshPage(){}function addRefreshStyles(){const style=document.createElement("style");style.textContent="\n            .silent-refresh-indicator {\n                position: fixed;\n                top: 20px;\n                right: 20px;\n                z-index: 9999;\n                opacity: 0;\n                transform: translateY(-10px);\n                transition: all 0.3s ease;\n                pointer-events: none;\n            }\n            \n            .silent-refresh-indicator.show {\n                opacity: 1;\n                transform: translateY(0);\n            }\n            \n            .refresh-spinner {\n                background: rgba(0, 0, 0, 0.8);\n                color: white;\n                padding: 8px 12px;\n                border-radius: 6px;\n                font-size: 14px;\n                display: flex;\n                align-items: center;\n                gap: 8px;\n            }\n            \n            .refresh-spinner i {\n                font-size: 12px;\n            }\n        ",document.head.appendChild(style)}function isCurrentMatch(matchId){return currentMatchId&&String(currentMatchId)===String(matchId)}function getStatusText(status){return{"1H":"H1","2H":"H2"}[status]||status||""}function addRefreshStyles(){const style=document.createElement("style");style.textContent="\n        .silent-refresh-indicator {\n            position: fixed;\n            top: 20px;\n            right: 20px;\n            z-index: 9999;\n            opacity: 0;\n            transform: translateY(-10px);\n            transition: all 0.3s ease;\n            pointer-events: none;\n        }\n        \n        .silent-refresh-indicator.show {\n            opacity: 1;\n            transform: translateY(0);\n        }\n        \n        .refresh-spinner {\n            background: rgba(0, 0, 0, 0.8);\n            color: white;\n            padding: 8px 12px;\n            border-radius: 6px;\n            font-size: 14px;\n            display: flex;\n            align-items: center;\n            gap: 8px;\n        }\n        \n        .refresh-spinner i {\n            font-size: 12px;\n        }\n    ",document.head.appendChild(style)}function isCurrentMatch(matchId){return currentMatchId&&String(currentMatchId)===String(matchId)}function getStatusText(status){return{"1H":"H1","2H":"H2"}[status]||status||""}return addRefreshStyles(),addRefreshStyles(),window.updateScoreDisplay=updateScoreDisplay,window.updateMatchInfo=updateMatchInfo,window.updateMatchStatus=updateMatchStatus,window.updateEventsDisplay=updateEventsDisplay,window.updateTextLiveDisplay=updateTextLiveDisplay,{init:async function init(
1){if(!connection){if("undefined"==typeof signalR)return console.warn("SignalR 未加载,实时更新功能不可用"),void MinuteTicker.start();try{connection=(new signalR.HubConnectionBuilder).withUrl("/matchHub").withAutomaticReconnect([0,2e3,5e3,1e4,3e4]).configureLogging(signalR.LogLevel.Warning).build(),function setupEventHandlers(){connection.onreceive=message=>{try{if(message.startsWith("[")){const messages=JSON.parse(message);Array.isArray(messages)&&messages.forEach((msg,index)=>{msg&&msg.arguments&&msg.arguments.forEach((arg,argIndex)=>{arg&&arg.type})})}else{const data=JSON.parse(message);data&&data.type}}catch(e){}},connection.on("MatchPatch",function(data){!function handleMatchPatch(data){const{matchId:matchId,version:version,serverEpoch:serverEpoch,patch:patch}=data;window.MatchStore&&window.MatchStore.isInitialized()?window.MatchStore.applyPatch(matchId,version,patch,serverEpoch)||console.warn("Patch 未应用,可能需要重同步"):function directUpdateFromPatch(matchId,patch){if(!patch)return;const payload={matchId:matchId,status:patch["status.short"],minute:patch.minute,homeScore:patch["score.home"],awayScore:patch["score.away"],homePossession:patch["stats.home.possession"],awayPossession:patch["stats.away.possession"],homeShots:patch["stats.home.shots"],awayShots:patch["stats.away.shots"],homeShotsOnTarget:patch["stats.home.shotsOnTarget"],awayShotsOnTarget:patch["stats.away.shotsOnTarget"],homeCorners:patch["stats.home.corners"],awayCorners:patch["stats.away.corners"],homeFouls:patch["stats.home.fouls"],awayFouls:patch["stats.away.fouls"],homeYellowCards:patch["stats.home.yellowCards"],awayYellowCards:patch["stats.away.yellowCards"],homeRedCards:patch["stats.home.redCards"],awayRedCards:patch["stats.away.redCards"],homeOffsides:patch["stats.home.offsides"],awayOffsides:patch["stats.away.offsides"],isLive:patch.isLive,events:patch.events,newEvents:patch.newEvents};void 0===payload.homeScore&&void 0===payload.awayScore||updateScoreDisplay(matchId,payload),void 0===payload.homeTeamLogo&&void 0===payload.awayTeamLogo&&void 0===payload.homeTeam&&void 0===payload.awayTeam||updateMatchInfo(matchId,payload),updateStatsDisplay(matchId,payload),void 0!==payload.status&&updateMatchStatus(matchId,payload),void 0!==payload.minute&&MinuteTicker.setMinute(matchId,payload.minute),void 0===payload.events&&void 0===payload.newEvents||updateEventsDisplay(matchId,payload)}(matchId,patch)}(data)}),connection.on("MatchStarted",function(data){window.MatchStore&&window.MatchStore.isInitialized()?(window.MatchStore.handleMatchStarted(data),function handleMatchStarted(data){const matchId=data.matchId,payload=data.payload;payload&&isTestData(payload.apiId),isCurrentMatch(matchId)?setTimeout(()=>{window.location.reload()},1e3):(isListPage()&&silentRefreshPage(),updateBroadcastStrip(matchId,payload,"started"),updateMatchCardStatus(matchId,"live",payload))}(data)):async function handleMatchStartedNotification(data){const matchId=data.matchId,version=data.version||0;if(!(lastVersions[matchId]&&lastVersions[matchId]>version)){if(lastVersions[matchId]=version,data.preview){updateBroadcastStrip(matchId,Object.assign({matchId:matchId},data.preview),"started")}isListPage()&&(pendingRefreshCount++,silentRefreshTimer&&clearTimeout(silentRefreshTimer),silentRefreshTimer=setTimeout(()=>{pendingRefreshCount=0,silentRefreshPage()},2e3))}}(data),callbacks.onMatchStarted.forEach(cb=>cb(data))}),connection.on("MatchTimeReached",function(data){!async function handleMatchTimeReachedNotification(data){const matchId=data.matchId,preview=data.preview;isListPage()&&(pendingRefreshCount++,silentRefreshTimer&&clearTimeout(silentRefreshTimer),silentRefreshTimer=setTimeout(()=>{pendingRefreshCount=0,silentRefreshPage()},2e3)),function updateMatchCardDelayedStatus(matchId,preview){const matchCard=document.querySelector(`[data-match-id="${matchId}"]`);if(matchCard){const statusElement=matchCard.querySelector(".match-status, .status");statusElement&&(statusElement.textContent=window.L&&window.L.T?window.L.T("match.streamNotStarted"):"直播未开始",statusElement.classList.add("status-delayed")),matchCard.querySelectorAll(".delayed-badge").forEach(function(badge){badge.remove()})}}(matchId),preview&&(preview.homeTeam,preview.awayTeam)}(data)}),connection.on("DataChanged",function(data){!async function handleDataChangedNotification(data){const matchId=data.matchId;data.version,void 0!==data.minute&&null!==data.minute&&MinuteTicker.setMinute(matchId,data.minute),isCurrentMatch(matchId)&&await fetchAndUpdateMatchDetail(matchId),isListPage()&&await async function fetchAndUpdateMatchCard(matchId){try{const response=await fetch(`/api/fixtures/${matchId}/summary`);if(!response.ok)return void console.error("拉取比赛卡片数据失败:",response.status);const data=await response.json();data&&(updateScoreDisplay(matchId,data),updateMatchCardStatus(matchId,data.isLive?"live":data.isFinished?"finished":"upcoming",data))}catch(err){console.error("拉取比赛卡片数据出错:",err)}}(matchId)}(data)}),connection.on("BatchDataChanged",function(data){!async function handleBatchDataChangedNotification(data){const batchVersion=data.batchVersion||0,changedMatches=data.matches||[];if(!(lastBatchVersion>=batchVersion)){lastBatchVersion=batchVersion;
1for(const m of changedMatches)m&&void 0!==m.matchId&&void 0!==m.minute&&null!==m.minute&&MinuteTicker.setMinute(m.matchId,m.minute);isListPage()&&await silentRefreshPage(),currentMatchId&&changedMatches.find(m=>m.matchId===parseInt(currentMatchId))&&await fetchAndUpdateMatchDetail(currentMatchId)}}(data)}),connection.on("BatchMatchesUpdated",function(updates){window.MatchStore&&window.MatchStore.isInitialized()&&window.MatchStore.handleBatchUpdates(updates)}),connection.on("ResyncRequired",function(data){window.MatchStore&&window.MatchStore.triggerResync(data.reason||"server_request")}),connection.on("LiveStatusUpdate",function(data){data.changes&&data.changes.length>0&&data.changes.forEach(change=>{}),updateLiveCountBadge(data.liveCount),updateFocusLiveCount(data.liveCount),data.changes&&data.changes.length>0&&data.changes.forEach(change=>{"removed"!==change.changeType?"added"!==change.changeType?change.isBeauty?updateBeautyLiveBadge(change):updateMatchLiveBadge(change):change.isBeauty?handleNewBeautyLiveAdded(change):handleNewLiveStreamAdded(change):change.isBeauty?handleBeautyLiveRemoved(change):updateMatchLiveBadge(change)}),tryAutoPlayOnLiveStatusChange(data.changes)}),connection.on("MatchUpdate",function(data){!function handleMatchUpdate(data){const matchId=data.matchId,payload=data.payload;payload.status&&["FT","AET","PEN","AWD","WO","CANC","ABD","PST"].includes(payload.status)&&isListPage()&&silentRefreshPage(),updateScoreDisplay(matchId,payload),updateStatsDisplay(matchId,payload),updateEventsDisplay(matchId,payload),updateMatchInfo(matchId,payload),updateMatchStatus(matchId,payload)}(data),callbacks.onMatchUpdate.forEach(cb=>cb(data))}),connection.on("JoinedMatchRoom",function(matchId){currentMatchId=matchId}),connection.on("StreamStarting",function(data){window.dispatchEvent(new CustomEvent("streamStarting",{detail:data}))}),connection.on("StreamReady",function(data){window.dispatchEvent(new CustomEvent("streamReady",{detail:data}))}),connection.on("StreamTimeout",function(data){window.dispatchEvent(new CustomEvent("streamTimeout",{detail:data}))}),connection.on("StreamFailed",function(data){window.dispatchEvent(new CustomEvent("streamFailed",{detail:data}))}),connection.on("SubscribedToListPage",function(data){}),connection.on("StreamRouteChanged",function(data){window.dispatchEvent(new CustomEvent("streamRouteChanged",{detail:data}));const message=data.message||"Đường truyền đã thay đổi, vui lòng tải lại trang";confirm(message+"\n\nNhấn OK để tải lại trang.")&&window.location.reload()}),connection.onreconnecting(error=>{isConnected=!1,showConnectionStatus("reconnecting")}),connection.onreconnected(connectionId=>{isConnected=!0,reconnectAttempts=0,showConnectionStatus("connected"),connection.invoke("SubscribeToListPage","list").catch(err=>{console.error("❌ 重连后订阅直播状态更新失败:",err)}),window.MatchStore&&window.MatchStore.init("live"),currentMatchId&&joinMatchRoom(currentMatchId),callbacks.onConnected.forEach(cb=>cb())}),connection.onclose(error=>{isConnected=!1,showConnectionStatus("disconnected"),callbacks.onDisconnected.forEach(cb=>cb()),reconnectAttempts<5&&setTimeout(()=>{reconnectAttempts++,startConnection()},3e3*reconnectAttempts)})}(),await startConnection(),MinuteTicker.start()}catch(err){console.error("初始化 WebSocket 失败:",err)}}},joinMatchRoom:joinMatchRoom,leaveMatchRoom:async function leaveMatchRoom(){if(connection&&isConnected)try{await connection.invoke("LeaveMatchRoom"),currentMatchId=null}catch(err){console.error("离开比赛房间失败:",err)}},on:function on(event,callback){callbacks[event]&&callbacks[event].push(callback)},off:function off(event,callback){if(callbacks[event]){const index=callbacks[event].indexOf(callback);index>-1&&callbacks[event].splice(index,1)}},isConnected:()=>isConnected,getCurrentMatchId:()=>currentMatchId,silentRefreshPage:silentRefreshPage,triggerSilentRefreshOnMessage:function triggerSilentRefreshOnMessage(){isListPage()&&silentRefreshPage()},refreshLiveCountFromServer:async function refreshLiveCountFromServer(){},updateLiveCount:function updateLiveCount(delta){const navLiveCount=document.getElementById("liveCount"),homeLink=navLiveCount?.closest("a.nav-link");if(navLiveCount){const currentCount=parseInt(navLiveCount.textContent)||0,newCount=Math.max(0,currentCount+delta);navLiveCount.textContent=newCount,navLiveCount.style.display=0===newCount?"none":"inline-block",homeLink&&(newCount>0?homeLink.classList.add("has-live"):homeLink.classList.remove("has-live"))}const focusLiveCount=document.getElementById("focusLiveCount");if(focusLiveCount){const currentCount=parseInt(focusLiveCount.textContent)||0,newCount=Math.max(0,currentCount+delta);focusLiveCount.textContent=newCount,"function"==typeof window.updateFocusLiveButtonState&&window.updateFocusLiveButtonState(newCount)}const liveGroups=domCache.getLiveCountGroups();
1liveGroups.statLiveItems.forEach(el=>{const match=el.textContent.match(/\d+/);if(match){const newCount=Math.max(0,parseInt(match[0])+delta);el.innerHTML=el.innerHTML.replace(/\d+/,newCount)}}),liveGroups.otherLiveCounts.forEach(el=>{const currentCount=parseInt(el.textContent)||0,newCount=Math.max(0,currentCount+delta);el.textContent=newCount})},updateAllLiveCounts:function updateAllLiveCounts(count){const navLiveCount=document.getElementById("liveCount"),homeLink=navLiveCount?.closest("a.nav-link");navLiveCount&&(navLiveCount.textContent=count,navLiveCount.style.display=0===count?"none":"inline-block",homeLink&&(count>0?homeLink.classList.add("has-live"):homeLink.classList.remove("has-live")));const focusLiveCount=document.getElementById("focusLiveCount");focusLiveCount&&(focusLiveCount.textContent=count,"function"==typeof window.updateFocusLiveButtonState&&window.updateFocusLiveButtonState(count));const liveGroups=domCache.getLiveCountGroups();liveGroups.statLiveItems.forEach(el=>{el.innerHTML=el.innerHTML.replace(/\d+/,count)}),liveGroups.liveBadges.forEach(el=>{el.textContent.includes("LIVE")&&(el.textContent=`LIVE ${count}`)}),liveGroups.filterTabBadges.forEach(el=>{const parentTab=el.closest(".filter-tab");parentTab&&(parentTab.href&&parentTab.href.includes("/live")||parentTab.querySelector(".live-dot"))&&(el.textContent=count)}),liveGroups.sidebarLiveCounts.forEach(el=>{el.textContent=count}),liveGroups.otherLiveCounts.forEach(el=>{el.textContent=count})},updateScoreDisplay:updateScoreDisplay,updateStatsDisplay:updateStatsDisplay,updateMatchStatus:updateMatchStatus,showMatchNotification:function showMatchNotification(type,payload,options={}){},muteMatchNotifications:function muteMatchNotifications(){setMatchNotificationMuted(!0),"function"==typeof window.showNotification&&window.showNotification("undefined"!=typeof L&&L.T?L.T("match.notificationsMuted"):"比赛通知已关闭,可在设置中重新开启","info")},unmuteMatchNotifications:function unmuteMatchNotifications(){setMatchNotificationMuted(!1)},isMatchNotificationMuted:function isMatchNotificationMuted(){try{return"true"===localStorage.getItem("matchNotificationMuted")}catch(e){return!1}}}}(),document.addEventListener("DOMContentLoaded",async function(){const matchIdMeta=document.querySelector('meta[name="match-id"]'),matchId=matchIdMeta?matchIdMeta.content:null;matchId&&MatchLiveHub.on("onConnected",function(){MatchLiveHub.joinMatchRoom(matchId)}),await MatchLiveHub.init(),matchId&&MatchLiveHub.isConnected()&&MatchLiveHub.joinMatchRoom(matchId),setInterval(()=>{"function"==typeof MatchLiveHub.refreshLiveCountFromServer&&MatchLiveHub.refreshLiveCountFromServer()},12e4)}),window.addEventListener("beforeunload",function(){MatchLiveHub.leaveMatchRoom()}),window.MatchPollingService=function(){let isPolling=!1,pollTimer=null,pollingEnabled=!1,lastVersion=0,lastLiveCount=0,consecutiveErrors=0,currentMatchId=null,currentPollInterval=5e3,emptyLiveStreak=0;const matchesCache=new Map;let matchVersions=function loadMatchVersions(){try{const stored=localStorage.getItem("matchVersions");if(stored)return JSON.parse(stored)}catch(e){console.warn("无法加载 matchVersions:",e)}return{}}();function saveMatchVersions(versions){try{matchVersions=versions,localStorage.setItem("matchVersions",JSON.stringify(versions))}catch(e){console.warn("无法保存 matchVersions:",e)}}function startPolling(){pollingEnabled=!0,scheduleNextPoll()}function stopPolling(){pollingEnabled=!1,pollTimer&&clearTimeout(pollTimer),pollTimer=null}function scheduleNextPoll(){pollingEnabled&&(pollTimer||(pollTimer=setTimeout(async()=>{pollTimer=null,await poll()},currentPollInterval)))}async function poll(){if(!isPolling){isPolling=!0;try{let url="/api/fixtures/poll";Object.keys(matchVersions).length>0&&(url=`/api/fixtures/poll?matchVersions=${encodeURIComponent(JSON.stringify(matchVersions))}`);const response=await fetch(url);if(!response.ok)throw new Error(`HTTP ${response.status}`);const data=await response.json();if(!data.success)throw new Error(data.message||"请求失败");consecutiveErrors=0,lastVersion=data.version,data.matchVersions&&saveMatchVersions(data.matchVersions),data.hasChanges&&function processChanges(data){const{isFullSnapshot:isFullSnapshot,changed:changed=[],removed:removed=[]}=data;if(isFullSnapshot){const oldCache=new Map(matchesCache);matchesCache.clear(),changed.forEach(match=>{const oldMatch=oldCache.get(match.matchId);matchesCache.set(match.matchId,match),updateMatchUI(match,oldMatch),!oldMatch&&match.isLive&&handleMatchAdded(match)}),oldCache.forEach((oldMatch,matchId)=>{matchesCache.has(matchId)||handleMatchRemoved(matchId,oldMatch)})}else changed.forEach(match=>{const oldMatch=matchesCache.get(match.matchId);matchesCache.set(match.matchId,match),updateMatchUI(match,oldMatch),!oldMatch&&match.isLive&&handleMatchAdded(match)}),removed.forEach(matchId=>{const oldMatch=matchesCache.get(matchId);matchesCache.delete(matchId),oldMatch&&handleMatchRemoved(matchId,oldMatch)})}(data),data.liveCount!==lastLiveCount&&(function updateLiveCountUI(count){const navLiveCount=document.getElementById("liveCount");navLiveCount&&(navLiveCount.textContent=count,navLiveCount.style.display=count>0?"inline-block":"none");const tabLiveCount=document.getElementById("liveCountTab");tabLiveCount&&(tabLiveCount.textContent=count,tabLiveCount.style.display=count>0?"inline-block":"none")}(data.liveCount),lastLiveCount=data.liveCount);const hasAnyChanged=Array.isArray(data.changed)&&data.changed.length>0;data.liveCount>0||hasAnyChanged?(emptyLiveStreak=0,currentPollInterval=5e3):(emptyLiveStreak++,emptyLiveStreak>=6&&(currentPollInterval=Math.min(6e4,2*currentPollInterval)))}catch(error){consecutiveErrors++,console.error("轮询失败:",error),consecutiveErrors>=5&&(console.warn("连续错误过多,暂停轮询30秒"),stopPolling(),setTimeout(()=>{consecutiveErrors=0,startPolling()},3e4))}finally{isPolling=!1,scheduleNextPoll()}}}function updateMatchUI(match,oldMatch){const matchId=match.matchId;if(window.MatchStore&&window.MatchStore.isInitialized()){const patch=function convertMatchToPatch(match){return{"status.short":match.status,"status.long":match.statusLong,minute:match.minute,"score.home":match.homeScore,"score.away":match.awayScore,"stats.home.possession":match.homePossession,"stats.away.possession":match.awayPossession,"stats.home.shots":match.homeShots,"stats.away.shots":match.awayShots,"stats.home.shotsOnTarget":match.homeShotsOnTarget,"stats.away.shotsOnTarget":match.awayShotsOnTarget,"stats.home.corners":match.homeCorners,"stats.away.corners":match.awayCorners,"stats.home.fouls":match.homeFouls,"stats.away.fouls":match.awayFouls,"stats.home.yellowCards":match.homeYellowCards,"stats.away.yellowCards":match.awayYellowCards,"stats.home.redCards":match.homeRedCards,"stats.away.redCards":match.awayRedCards,"stats.home.offsides":match.homeOff
1sides,"stats.away.offsides":match.awayOffsides,isLive:match.isLive,events:match.events}}(match);return void window.MatchStore.applyPatch(matchId,Date.now(),patch,null)}const payload={matchId:match.matchId,homeScore:match.homeScore,awayScore:match.awayScore,status:match.status,minute:match.minute,homeTeam:match.homeTeam,awayTeam:match.awayTeam,homeTeamLogo:match.homeTeamLogo,awayTeamLogo:match.awayTeamLogo,leagueName:match.leagueName,leagueLogo:match.leagueLogo,homePossession:match.homePossession,awayPossession:match.awayPossession,homeShots:match.homeShots,awayShots:match.awayShots,homeShotsOnTarget:match.homeShotsOnTarget,awayShotsOnTarget:match.awayShotsOnTarget,homeCorners:match.homeCorners,awayCorners:match.awayCorners,homeFouls:match.homeFouls,awayFouls:match.awayFouls,homeYellowCards:match.homeYellowCards,awayYellowCards:match.awayYellowCards,homeRedCards:match.homeRedCards,awayRedCards:match.awayRedCards,homeOffsides:match.homeOffsides,awayOffsides:match.awayOffsides,isLive:match.isLive,events:normalizeEvents(match)};"function"==typeof window.updateScoreDisplay&&window.updateScoreDisplay(matchId,payload),"function"==typeof window.updateStatsDisplay&&window.updateStatsDisplay(matchId,payload),"function"==typeof window.updateMatchStatus&&window.updateMatchStatus(matchId,payload),"function"==typeof window.updateEventsDisplay&&window.updateEventsDisplay(matchId,payload),"function"==typeof window.updateTextLiveDisplay&&window.updateTextLiveDisplay(matchId,payload),function updateBasicUI(matchId,match){if(isCurrentMatch(matchId)){const heroScore=document.querySelector(".match-detail-hero .score-display");if(heroScore){const homeScore=heroScore.querySelector(".home-score"),awayScore=heroScore.querySelector(".away-score");homeScore&&(homeScore.textContent=match.homeScore??0),awayScore&&(awayScore.textContent=match.awayScore??0)}}const card=document.querySelector(`[data-match-id="${matchId}"], .match-card[href*="/${matchId}"]`);if(card){const scoreHome=card.querySelector(".score-home"),scoreAway=card.querySelector(".score-away");scoreHome&&(scoreHome.textContent=match.homeScore??0),scoreAway&&(scoreAway.textContent=match.awayScore??0),match.isLive?(card.classList.add("live"),card.classList.remove("finished","upcoming")):["FT","AET","PEN"].includes(match.status)&&(card.classList.add("finished"),card.classList.remove("live","upcoming"))}}(matchId,match),oldMatch&&(!oldMatch.isLive&&match.isLive&&handleMatchStarted(match),oldMatch.isLive&&!match.isLive&&["FT","AET","PEN"].includes(match.status)&&function handleMatchEnded(match){match.homeTeam,match.homeScore,match.awayScore,match.awayTeam,isCurrentMatch(match.matchId)&&stopPolling(),silentRefresh
1IfNeeded()}(match))}function normalizeEvents(match){return match&&Array.isArray(match.events)?match.events.map(e=>{const minute=e.minute??e.Minute??e.elapsed??e.Elapsed??0,rawType=e.type??e.Type??"",detail=e.detail??e.Detail??"",player=e.player??e.Player??e.PlayerName??"",teamName=e.teamName??e.TeamName??e.team??e.Team??"";let team=e.team;"Home"!==team&&"Away"!==team&&(team=teamName&&teamName===match.homeTeam?"Home":teamName&&teamName===match.awayTeam?"Away":"");let type=rawType;return"Card"===rawType&&(type=(detail||"").toLowerCase().includes("red")?"Red":"Yellow"),"subst"===rawType&&(type="sub"),{minute:parseInt(minute,10)||0,type:type,team:team,player:player,detail:detail}}).filter(e=>"number"==typeof e.minute):null}function handleMatchStarted(match){match.homeTeam,match.awayTeam,function isNotificationMuted(){try{return"true"===localStorage.getItem("matchNotificationMuted")}catch(e){return!1}}()||(function hasNotifiedMatchStart(matchId){try{const notified=JSON.parse(localStorage.getItem("notifiedMatchStarts")||"{}"),today=(new Date).toISOString().split("T")[0];return notified[matchId]===today}catch(e){return!1}}(match.matchId)?match.matchId:(function markMatchStartNotified(matchId){try{const notified=JSON.parse(localStorage.getItem("notifiedMatchStarts")||"{}"),today=(new Date).toISOString().split("T")[0],cleaned={};
1Object.keys(notified).forEach(id=>{notified[id]===today&&(cleaned[id]=today)}),cleaned[matchId]=today,localStorage.setItem("notifiedMatchStarts",JSON.stringify(cleaned))}catch(e){console.warn("无法保存通知记录:",e)}}(match.matchId),window.MatchLiveHub&&"function"==typeof window.MatchLiveHub.showMatchNotification&&window.MatchLiveHub.showMatchNotification("started",{matchId:match.matchId,homeTeam:match.homeTeam,awayTeam:match.awayTeam,homeTeamLogo:match.homeTeamLogo,awayTeamLogo:match.awayTeamLogo,homeScore:match.homeScore||0,awayScore:match.awayScore||0,league:match.leagueName,leagueLogo:match.leagueLogo}),silentRefreshIfNeeded()))}function handleMatchAdded(match){match.isLive&&handleMatchStarted(match),silentRefreshIfNeeded()}function handleMatchRemoved(matchId,oldMatch){oldMatch&&isCurrentMatch(matchId)&&stopPolling(),silentRefreshIfNeeded()}function silentRefreshIfNeeded(){const path=window.location.pathname.toLowerCase();("/"===path||"/home"===path||path.startsWith("/match"))&&window.MatchLiveHub&&"function"==typeof window.MatchLiveHub.silentRefreshPage&&window.MatchLiveHub.silentRefreshPage()}function isCurrentMatch(matchId){return currentMatchId&&parseInt(currentMatchId)===parseInt(matchId)}return{init:function init(){return this},startPolling:startPolling,stopPolling:stopPolling,poll:poll,forceRefresh:function forceRefresh(){lastVersion=0,matchVersions={},saveMatchVersions({}),matchesCache.clear(),poll()},setCurrentMatchId:function setCurrentMatchId(matchId){currentMatchId=matchId},getMatch:function getMatch(matchId){return matchesCache.get(matchId)},getAllMatches:function getAllMatches(){return Array.from(matchesCache.values())},getCurrentMatchId:()=>currentMatchId,isConnected:()=>!0,isEnabled:()=>pollingEnabled}}(),document.addEventListener("DOMContentLoaded",function(){setTimeout(()=>{window.MatchPollingService.init()},1e3)}),function(){"use strict";let isInitialized=!1,lastMessageTime=0;function init(){isInitialized||(document.addEventListener("DOMContentLoaded",function(){setupSignalRListeners(),setupCustomEventListeners()}),"loading"!==document.readyState&&(setupSignalRListeners(),setupCustomEventListeners()),isInitialized=!0)}function setupSignalRListeners(){const checkConnection=setInterval(()=>{void 0!==window.signalR&&window.chatConnection&&(clearInterval(checkConnection),window.chatConnection&&(window.chatConnection.on("ReceiveMessage",handleGlobalMessage),window.chatConnection.on("SystemMessage",handleGlobalMessage)))},100);setTimeout(()=>clearInterval(checkConnection),1e4)}function setupCustomEventListeners(){document.addEventListener("chatMessageReceived",handleCustomMessageEvent),document.addEventListener("systemMessageReceived",handleCustomMessageEvent),window.addEventListener("storage",handleStorageEvent)}function handleGlobalMessage(msg){const now=Date.now();now-lastMessageTime<5e3||(lastMessageTime=now,void 0!==window.MatchLiveHub&&window.MatchLiveHub.refreshLiveCountFromServer&&window.MatchLiveHub.refreshLiveCountFromServer())}function handleCustomMessageEvent(event){void 0!==window.MatchLiveHub&&window.MatchLiveHub.refreshLiveCountFromServer&&window.MatchLiveHub.refreshLiveCountFromServer()}function handleStorageEvent(event){event.key}function triggerSilentRefresh(){(function isListPage(){const path=window.location.pathname.toLowerCase();return"/"===path||"/home"===path||"/home/index"===path||path.startsWith("/match")&&!path.includes("/details")})()&&(void 0!==window.MatchLiveHub&&window.MatchLiveHub.silentRefreshPage?window.MatchLiveHub.silentRefreshPage():function fallbackRefresh(){try{[".live-matches-section",".hot-matches-section","#liveMatchesContainer",".matches-grid"].forEach(selector=>{const element=document.querySelector(selector);element&&(element.style.transition="opacity 0.3s",element.style.opacity="0.7",setTimeout(()=>{element.style.opacity="1"},300))})}catch(error){console.warn("回退刷新失败:",error)}}(),function notifyOtherTabs(){try{localStorage.setItem("chatMessageReceived",Date.now().toString()),setTimeout(()=>localStorage.removeItem("chatMessageReceived"),100)}catch(error){console.warn("通知其他标签页失败:",error)}}())}window.GlobalMessageListener={init:init,manualRefresh:function manualRefresh(){lastMessageTime=0,triggerSilentRefresh()},triggerSilentRefresh:triggerSilentRefresh},init()}(),document.addEventListener("DOMContentLoaded",function(){initUserMenu(),initMobileUserMenu(),initViewingHistory()});const VIEWING_MIN_SECONDS=30;let viewingStartTime=null,currentMatchId=null,viewingFlushedSeconds=0,viewingFlushTimer=null,viewingLeaveBound=!1;function isViewingUserLoggedIn(){return!!document.querySelector('[data-auth="true"]')}function resolveWatchingMatchId(){const focus=document.getElementById("focusPlayerContainer"),live=document.getElementById("livePlayerContainer"),meta=document.querySelector('meta[name="match-id"]'),raw=focus&&focus.dataset.matchI
1d||live&&live.dataset.matchId||meta&&meta.content||"",id=parseInt(raw,10);return!id||id<=0?null:document.getElementById("focusLivePlayer")||document.getElementById("livePlayer")?focus&&focus.classList.contains("focus-no-stream")?null:id:null}function viewingElapsedSeconds(){return viewingStartTime?Math.floor((Date.now()-viewingStartTime)/1e3):0}function postViewingRecord(matchId,durationSeconds){!matchId||durationSeconds<1||fetch("/profile/viewing/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({matchId:matchId,durationSeconds:durationSeconds,url:window.location.href}),keepalive:!0,credentials:"same-origin"}).catch(function(){})}function saveViewingHistory(){if(!viewingStartTime||!currentMatchId)return;const total=viewingElapsedSeconds();if(total<30)return;const delta=total-viewingFlushedSeconds;delta<1||(postViewingRecord(currentMatchId,delta),viewingFlushedSeconds=total)}function startViewingTracking(matchId){const id=parseInt(matchId,10);!id||id<=0||!isViewingUserLoggedIn()||currentMatchId===id&&viewingStartTime||(currentMatchId&&currentMatchId!==id&&saveViewingHistory(),currentMatchId=id,viewingStartTime=Date.now(),viewingFlushedSeconds=0,viewingFlushTimer||(viewingFlushTimer=setInterval(saveViewingHistory,15e3)))}function stopViewingTracking(){saveViewingHistory(),currentMatchId=null,viewingStartTime=null,viewingFlushedSeconds=0}function syncViewingTracking(){if(!isViewingUserLoggedIn())return;const id=resolveWatchingMatchId();id?startViewingTracking(id):currentMatchId&&stopViewingTracking()}function bindViewingLeaveHandlers(){viewingLeaveBound||(viewingLeaveBound=!0,window.addEventListener("pagehide",saveViewingHistory),window.addEventListener("beforeunload",saveViewingHistory),document.addEventListener("visibilitychange",function(){"hidden"===document.visibilityState&&saveViewingHistory()}))}function initViewingHistory(){bindViewingLeaveHandlers(),syncViewingTracking(),setInterval(syncViewingTracking,5e3),["focusPlayerContainer","livePlayerContainer"].forEach(function(id){var el=document.getElementById(id);el&&window.MutationObserver&&new MutationObserver(syncViewingTracking).observe(el,{attributes:!0,attributeFilter:["data-match-id","class"]})})}function toggleMatchFavorite(matchId,btn){const matchUrl=btn?.closest("a.match-row")?.href||btn?.closest(".mx-row")?.querySelector("a.mx-teams-link, a.mx-center")?.href||window.location.href;fetch("/profile/favorites/toggle",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({itemId:matchId,itemType:"Match",url:matchUrl})}).then(response=>response.json()).then(data=>{if(data.success){const clickedBtn=btn||null;if(clickedBtn){clickedBtn.classList.toggle("active",!!data.isFavorited);const useEl=clickedBtn.querySelector("use");useEl&&useEl.setAttribute("href",data.isFavorited?"#icon-star-filled":"#icon-star");const icon=clickedBtn.querySelector("i");icon&&(icon.classList.toggle("active",!!data.isFavorited),icon.classList.remove("far","fas"),icon.classList.add(data.isFavorited?"fas":"far"),icon.classList.add("fa-star"))}const favoriteIcon=document.getElementById("favoriteIcon");favoriteIcon&&(favoriteIcon.classList.toggle("active",!!data.isFavorited),favoriteIcon.parentElement?.classList.toggle("active",!!data.isFavorited));const added=window.L?.T?window.L.T("match.favorited"):"收藏成功!",removed=window.L?.T?window.L.T("match.unfavorited"):"已取消收藏";showToast(data.isFavorited?added:removed,"success")}else data.message&&-1!==data.message.indexOf("未登录")?showLoginRequiredModal():showToast(data.message||(window.L?.T?window.L.T("common.operationFailed"):"操作失败"),"error")}).catch(()=>{showToast(window.L?.T?window.L.T("user.networkError"):"网络错误,请重试","error")})}function handleFavoriteClick(matchId,btn){"true"===btn?.dataset.auth?toggleMatchFavorite(matchId,btn):showLoginRequiredModal()}function handleNavbarFavoriteClick(btn){"true"===btn?.dataset.auth?window.location.href="/profile/favorites":showLoginRequiredModal()}function showLoginRequiredModal(){if(window.AuthModal)return void window.AuthModal.open("login");const returnUrl=encodeURIComponent(window.location.pathname+window.location.search);window.location.href="/Account/Login?returnUrl="+returnUrl}function reportVideoError(matchId){const modal=document.createElement("div");
1modal.className="share-modal",modal.dataset.matchId=(matchId??"").toString(),modal.innerHTML=`\n        <div class="share-modal-content report-modal-content">\n            <div class="share-modal-header">\n                <span class="share-modal-title">${window.L?.T?window.L.T("match.report.title"):"视频报错"}</span>\n                <button class="share-modal-close" onclick="closeReportModal(this)">\n                    <i class="fas fa-times"></i>\n                </button>\n            </div>\n            <div class="report-options">\n                <div class="report-option" data-type="no-video">\n                    <i class="fas fa-video-slash"></i>\n                    <span>${window.L?.T?window.L.T("match.report.noVideo"):"视频无法播放"}</span>\n                </div>\n                <div class="report-option" data-type="lag">\n                    <i class="fas fa-spinner"></i>\n                    <span>${window.L?.T?window.L.T("match.report.lag"):"视频卡顿"}</span>\n                </div>\n                <div class="report-option" data-type="no-sound">\n                    <i class="fas fa-volume-mute"></i>\n                    <span>${window.L?.T?window.L.T("match.report.noSound"):"没有声音"}</span>\n                </div>\n                <div class="report-option" data-type="wrong-source">\n                    <i class="fas fa-exchange-alt"></i>\n                    <span>${window.L?.T?window.L.T("match.report.lowQuality"):"视频源错误"}</span>\n                </div>\n                <div class="report-option" data-type="other">\n                    <i class="fas fa-question-circle"></i>\n                    <span>${window.L?.T?window.L.T("common.other"):"其他问题"}</span>\n                </div>\n            </div>\n            <button class="report-submit" onclick="submitProfileReport(this)">${window.L?.T?window.L.T("match.report.submit"):"提交反馈"}</button>\n        </div>\n    `,document.body.appendChild(modal);const submitBtn=modal.querySelector(".report-submit");submitBtn&&(submitBtn.onclick=function(){submitProfileReport(this)}),setTimeout(()=>modal.classList.add("show"),10),modal.querySelectorAll(".report-option").forEach(option=>{option.addEventListener("click",function(){modal.querySelectorAll(".report-option").forEach(o=>o.classList.remove("selected")),this.classList.add("selected")})}),modal.addEventListener("click",function(e){e.target===modal&&closeReportModal(modal.querySelector(".share-modal-close"))})}function closeReportModal(btn){const modal=btn.closest(".share-modal");modal.classList.remove("show"),setTimeout(()=>modal.remove(),300)}function submitProfileReport(btn){if(!btn)return;const modal=btn.closest(".share-modal");if(!modal)return console.error("❌ 无法找到模态框"),void("function"==typeof showToast?showToast("提交失败,请重试","error"):alert("提交失败,请重试"));const matchIdRaw=modal.dataset.matchId,matchId=matchIdRaw?Number(matchIdRaw):NaN;if(!Number.isFinite(matchId)||matchId<=0)return console.error("❌ matchId 无效:",matchIdRaw),void("function"==typeof showToast?showToast("提交失败,请刷新页面重试","error"):alert("提交失败,请刷新页面重试"));const selectedOption=modal.querySelector(".report-option.selected");if(!selectedOption)return void("function"==typeof showToast?showToast("请选择问题类型","error"):alert("请选择问题类型"));const errorType=selectedOption.dataset.type;fetch("/Match/ReportError",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({matchId:matchId,errorType:errorType})}).then(response=>response.json()).then(data=>{showToast("感谢您的反馈,我们会尽快处理","success"),closeReportModal(btn)}).catch(()=>{showToast("反馈已提交","success"),closeReportModal(btn)})}function shareMatch(matchId){const currentUrl=window.location.href,title=document.title;if("function"==typeof shareUrlWithFallback)return void shareUrlWithFallback(currentUrl,title);const shareData={title:title,text:title,url:currentUrl};"function"==typeof shouldUseNativeShare&&shouldUseNativeShare(shareData)?navigator.share(shareData).catch(function(err){err&&"AbortError"===err.name||copyShareLink(currentUrl)}):copyShareLink(currentUrl)}function closeShareModal(btn){const modal=btn.closest(".share-modal");modal.classList.remove("show"),setTimeout(()=>modal.remove(),300)}function shareToWechat(){showToast("请使用微信扫一扫分享","success")}function shareToWeibo(title,url){window.open(`https://service.weibo.com/share/share.php?title=${title}&url=${url}`,"_blank","width=600,height=400")}function shareToQQ(title,url){window.open(`https://connect.qq.com/widget/shareqq/index.html?title=${title}&url=${url}`,"_blank","width=600,height=400")}function copyShareLink(url){navigator.clipboard?navigator.clipboard.writeText(url).then(()=>{showToast("链接已复制到剪贴板","success")}
1).catch(()=>{fallbackCopyLink(url)}):fallbackCopyLink(url)}function fallbackCopyLink(url){const textarea=document.createElement("textarea");textarea.value=url,textarea.style.position="fixed",textarea.style.opacity="0",document.body.appendChild(textarea),textarea.select();try{document.execCommand("copy"),showToast("链接已复制到剪贴板","success")}catch(e){showToast("复制失败,请手动复制","error")}document.body.removeChild(textarea)}function showToast(message,type="success"){const existingToast=document.querySelector(".toast-notification");existingToast&&existingToast.remove();const toast=document.createElement("div");toast.className=`toast-notification ${type}`,toast.innerHTML=`\n        <i class="fas fa-${"success"===type?"check-circle":"exclamation-circle"}"></i>\n        <span>${message}</span>\n    `,document.body.appendChild(toast),setTimeout(()=>toast.classList.add("show"),10),setTimeout(()=>{toast.classList.remove("show"),setTimeout(()=>toast.remove(),300)},3e3)}function togglePiP(){const video=document.getElementById("livePlayer");video&&(document.pictureInPictureElement?document.exitPictureInPicture().catch(()=>{}):document.pictureInPictureEnabled?video.requestPictureInPicture().catch(()=>{showToast("您的浏览器不支持画中画","error")}):showToast("您的浏览器不支持画中画","error"))}function initProfilePage(){var page=document.querySelector(".profile-page");if(page&&"1"!==page.dataset.profileReady){page.dataset.profileReady="1";var allowedTabs=["overview","edit","rank","follows","favorites","history","settings"];document.querySelectorAll("[data-tab]").forEach(function(el){el.addEventListener("click",function(e){var tab=el.dataset.tab;tab&&page.contains(el)&&(e.preventDefault(),e.stopPropagation(),activateProfileTab(tab))})}),activateProfileTab(function resolveInitialTab(){var tab=tabFromHash()||tabFromPath(),next=profileUrl(tab);return((location.pathname.replace(/\/+$/,"")||"/")!==next||location.hash)&&history.replaceState({profileTab:tab},"",next),tab}(),!1),window.__profileHistoryBound||(window.__profileHistoryBound=!0,window.addEventListener("popstate",function(){document.querySelector(".profile-page")&&activateProfileTab(tabFromPath(),!1)}),window.addEventListener("hashchange",function(){if(document.querySelector(".profile-page")){var hashTab=tabFromHash();hashTab&&activateProfileTab(hashTab)}})),initProfileListTools()}function normalizeTab(tab){return tab=(tab||"").toLowerCase(),-1===allowedTabs.indexOf(tab)?"overview":tab}function profileUrl(tab){return"/profile/"+normalizeTab(tab)}function tabFromPath(){var parts=location.pathname.replace(/\/+$/,"").split("/"),last=(parts[parts.length-1]||"").toLowerCase();return"profile"===last?"overview":normalizeTab(last)}function tabFromHash(){var hash=(location.hash||"").replace(/^#/,"").toLowerCase();return-1===allowedTabs.indexOf(hash)?null:hash}function activateProfileTab(tab,updateUrl){tab=normalizeTab(tab),document.querySelectorAll(".profile-side-nav-item[data-tab]").forEach(function(item){item.classList.toggle("active",item.dataset.tab===tab)});var identity=document.getElementById("profileIdentityNav");if(identity&&identity.classList.toggle("active","overview"===tab),document.querySelectorAll(".profile-panel").forEach(function(panel){panel.classList.toggle("active",panel.id==="panel-"+tab)}),window.ProfileLoading&&window.ProfileLoading.show(tab),!1!==updateUrl){var next=profileUrl(tab);((location.pathname.replace(/\/+$/,"")||"/")!==next||location.hash)&&history.pushState({profileTab:tab},"",next)}}}function initProfileListTools(){function t(key,fallback){var value=window.L&&window.L.T?window.L.T(key):null;return value&&value!==key?value:fallback}function createList(key,itemSelector){var list=document.getElementById(key+"List"),search=document.getElementById(key+"Search");return list&&search?{key:key,list:list,search:search,sort:document.getElementById(key+"Sort"),pager:document.getElementById(key+"Pager"),noResults:document.getElementById(key+"NoResults"),searchWrap:document.getElementById(key+"SearchWrap"),itemSelector:itemSelector,page:1}:null}var lists={follows:createList("follows",".follows-item"),favorites:createList("favorites",".content-item"),history:createList("history",".content-item")};function parseTime(value){if(!value)return Number.POSITIVE_INFINITY;var time=Date.parse(value);return isNaN(time)?Number.POSITIVE_INFINITY:time}function favoriteGroup(el){return"1"===el.dataset.live?0:"1"===el.dataset.upcoming?1:2}function renderList(cfg){var items=function sortItems(cfg,items){if(!cfg.sort)return items;var mode=cfg.sort.value||"live",ranked=items.slice().sort(function(a,b){if("recent"===mode)return parseTime(b.dataset.createdAt)-parseTime(a.dataset.createdAt);if("upcoming"===mode){var ua="1"===a.dataset.upcoming?0:1,ub="1"===b.dataset.upcoming?0:1;return ua!==ub?ua-ub:parseTime(a.dataset.matchTime)-parseTime(b.dataset.matchTime)}if("matchTime"===mode)return parseTime(a.dataset.matchTime)-parseTime(b.dataset.matchTime);if("leagues"===mode){var la=(a.dataset.league||"").toLowerCase(),lb=(b.dataset.league||"").toLowerCase();return la.localeCompare(lb)||parseTime(a.dataset.matchTime)-parseTime(b.dataset.matchTime)}var ga=favoriteGroup(a),gb=favoriteGroup(b);return ga!==gb?ga-gb:2===ga?parseTime(b.dataset.matchTime)-parseTime(a.dataset.matchTime):parseTime(a.dataset.matchTime)-parseTime(b.dataset.matchTime)});return ranked.forEach(function(el){cfg.list.appendChild(el)}),ranked}(cfg,function itemsOf(cfg){return Array.from(cfg.list.querySelectorAll(cfg.itemSelector))}(cfg)),query=(cfg.search.value||"").trim().toLowerCase();
1cfg.searchWrap&&(cfg.searchWrap.hidden=0===items.length),cfg.list.classList.toggle("is-filtered",!!query);var matched=items.filter(function(el){var text=(el.textContent||"").replace(/\s+/g," ").toLowerCase(),ok=!query||-1!==text.indexOf(query);return el.classList.toggle("is-search-hidden",!ok),ok}),totalPages=Math.max(1,Math.ceil(matched.length/20));cfg.page>totalPages&&(cfg.page=totalPages);var start=20*(cfg.page-1),end=start+20;matched.forEach(function(el,i){el.classList.toggle("is-page-hidden",i<start||i>=end)}),cfg.noResults&&(cfg.noResults.hidden=!(query&&0===matched.length&&items.length>0)),function renderPager(cfg,matchCount,totalPages){if(cfg.pager){if(matchCount<=20)return cfg.pager.hidden=!0,void(cfg.pager.innerHTML="");cfg.pager.hidden=!1;var html='<button type="button" data-page="'+(cfg.page-1)+'"'+(cfg.page<=1?" disabled":"")+">"+t("user.prevPage","Previous")+"</button>";(function pageWindow(current,total){if(total<=7)return Array.from({length:total},function(_,i){return i+1});var pages=[1],start=Math.max(2,current-1),end=Math.min(total-1,current+1);start>2&&pages.push("...");for(var i=start;i<=end;i++)pages.push(i);return end<total-1&&pages.push("..."),pages.push(total),pages})(cfg.page,totalPages).forEach(function(p){html+="..."===p?'<span class="profile-list-pager-ellipsis">…</span>':'<button type="button" class="'+(p===cfg.page?"is-active":"")+'" data-page="'+p+'">'+p+"</button>"}),html+='<button type="button" data-page="'+(cfg.page+1)+'"'+(cfg.page>=totalPages?" disabled":"")+">"+t("user.nextPage","Next")+"</button>",cfg.pager.innerHTML=html}}(cfg,matched.length,totalPages)}Object.keys(lists).forEach(function(key){var cfg=lists[key];cfg&&(cfg.search.addEventListener("input",function(){cfg.page=1,renderList(cfg)}),cfg.sort&&cfg.sort.addEventListener("change",function(){cfg.page=1,renderList(cfg)}),cfg.pager&&cfg.pager.addEventListener("click",function(e){var btn=e.target.closest("[data-page]");if(btn&&!btn.disabled){var next=parseInt(btn.dataset.page,10);!next||next<1||(cfg.page=next,renderList(cfg))}}),renderList(cfg))}),window.ProfileListTools={refresh:function(key){var cfg=lists[key];cfg&&renderList(cfg)}}}window.ProfileLoading=function(){var tabs=["overview","rank","follows","favorites","history"],tokens={},waiters={};function setLoading(tab,isLoading){var el=function panelEl(tab){return document.getElementById("panel-"+tab)}(tab);el&&(el.classList.toggle("is-loading",!!isLoading),el.setAttribute("aria-busy",isLoading?"true":"false"))}function waitFor(tab,promise){-1!==tabs.indexOf(tab)&&(waiters[tab]=Promise.resolve(waiters[tab]).then(function(){return promise}).catch(function(){}))}return window.__profileRankReady&&waitFor("rank",window.__profileRankReady),{show:function show(tab){if(-1!==tabs.indexOf(tab)){var id=tokens[tab]=(tokens[tab]||0)+1;setLoading(tab,!0);var started=Date.now();Promise.resolve(waiters[tab]).finally(function(){var wait=Math.max(0,320-(Date.now()-started));setTimeout(function(){tokens[tab]===id&&setLoading(tab,!1)},wait)})}},waitFor:waitFor,setLoading:setLoading}}(),window.initProfilePage=initProfilePage,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",initProfilePage):document.querySelector(".profile-page")&&initProfilePage(),function(){"use strict";if(!window.StreamerFollow){var followedIds=new Set,i18n_follow="Follow",i18n_unfollow="Unfollow",i18n_followed="Followed",i18n_unfollowed="Unfollowed",browseState={page:1,q:"",total:0,timer:null};document.addEventListener("DOMContentLoaded",function(){!function loadFollowedIds(){document.querySelector('[data-auth="true"].streamer-follow-fab, [data-auth="true"].streamer-follow-btn')?fetch("/profile/follows/ids").then(function(res){return res.ok?res.json():null}).then(function(data){data&&data.success&&data.ids&&(followedIds=new Set(data.ids.map(Number)),document.querySelectorAll(".streamer-follow-fab[data-streamer-id], .streamer-follow-btn[data-streamer-id]").forEach(function(btn){var id=parseInt(btn.dataset.streamerId,10);id&&followedIds.has(id)&&applyButtonState(btn,!0)}),refreshCardFollowRanks())}).catch(function(){}):Promise.resolve()}(),function initFollowsSortable(){var list=document.querySelector("#panel-follows .follows-sortable");if(list&&"true"!==list.dataset.sortableReady){list.dataset.sortableReady="true";var drag=null;list.addEventListener("pointerdown",function(e){if(0===e.button){var handle=e.target.closest(".follows-drag-handle");if(handle&&list.contains(handle)){var item=handle.closest(".follows-item");item&&(e.preventDefault(),drag={item:item,pointerId:e.pointerId,startY:e.clientY,active:!1,startOrder:currentIds().join(",")},document.addEventListener("pointermove",onPointerMove,{passive:!1}),document.addEventListener("pointerup",onPointerUp),document.addEventListener("pointercancel",onPointerUp))}}})}function getAllItems(){return Array.from(list.querySelectorAll(":scope > .follows-item"))}function currentIds(){return getAllItems().map(function(el){return parseInt(el.dataset.streamerId,10)}).filter(function(id){return id>0})}function clearDropMarks(){list.querySelectorAll(".follows-drop-before, .follows-drop-after").forEach(function(el){el.classList.remove("follows-drop-before","follows-drop-after")})}function moveByClientY(clientY){if(drag){var item=drag.item,others=function getItems(){return getAllItems().filter(function(el){return!el.classList.contains("is-search-hidden")&&!el.classList.contains("is-page-hidden")})}().filter(function(el){return el!==item});if(clearDropMarks(),others.length){for(var i=0;i<others.length;i++){var rect=others[i].getBoundingClientRect();if(clientY<rect.top+rect.height/2)return others[i].classList.add("follows-drop-before"),void(item.nextSibling!==others[i]&&list.insertBefore(item,others[i])
1)}var last=others[others.length-1];last.classList.add("follows-drop-after"),item.previousSibling!==last&&list.appendChild(item)}}}function onPointerMove(e){drag&&e.pointerId===drag.pointerId&&(!drag.active&&Math.abs(e.clientY-drag.startY)<5||(drag.active||(drag.active=!0,drag.item.classList.add("is-dragging"),list.classList.add("is-reordering")),e.preventDefault(),moveByClientY(e.clientY)))}function onPointerUp(e){if(drag&&e.pointerId===drag.pointerId){document.removeEventListener("pointermove",onPointerMove),document.removeEventListener("pointerup",onPointerUp),document.removeEventListener("pointercancel",onPointerUp);var changed=drag.active&&drag.startOrder!==currentIds().join(",");drag.item.classList.remove("is-dragging"),list.classList.remove("is-reordering"),clearDropMarks(),drag=null,changed&&function persistOrder(){var ids=currentIds();ids.length&&fetch("/profile/follows/reorder",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({streamerIds:ids})}).then(function(res){return res.json()}).then(function(data){data&&!data.success&&"function"==typeof showToast&&showToast(data.message||"Failed","error")}).catch(function(){"function"==typeof showToast&&showToast("Network error","error")})}()}}}(),function initBrowseStreamersModal(){var modal=document.getElementById("browseStreamersModal");modal&&"true"!==modal.dataset.ready&&(modal.dataset.ready="true",document.getElementById("followsBrowseBtn")?.addEventListener("click",openBrowseModal),document.getElementById("followsBrowseIconBtn")?.addEventListener("click",openBrowseModal),modal.addEventListener("click",function(e){if(e.target.closest("[data-browse-close]"))closeBrowseModal();else{var pageBtn=e.target.closest("[data-browse-page]");if(pageBtn&&!pageBtn.disabled){var pages=Math.max(1,Math.ceil(browseState.total/300));"prev"===pageBtn.dataset.browsePage&&(browseState.page=Math.max(1,browseState.page-1)),"next"===pageBtn.dataset.browsePage&&(browseState.page=Math.min(pages,browseState.page+1)),loadBrowseStreamers()}}}),document.addEventListener("keydown",function(e){"Escape"!==e.key||modal.hidden||closeBrowseModal()}))}(),syncFollowsEmptyState(),function initStreamerPagedLists(){document.querySelectorAll("[data-paged-list]").forEach(initStreamerPagedList)}();var pending=window.__pendingPlayerStreamer,activeCard=document.querySelector(".focus-card.active, .beauty-card.active, .streamer-btn.active"),fab=document.getElementById("streamerFollowFab"),container=document.getElementById("focusPlayerContainer")||document.getElementById("livePlayerContainer");if(pending)setPlayerStreamer(pending.streamerId,void 0,pending.card||activeCard),window.__pendingPlayerStreamer=null;else if(activeCard&&parseInt(activeCard.dataset.streamerId,10))setPlayerStreamer(activeCard.dataset.streamerId,void 0,activeCard);else{var initialId=fab&&fab.dataset.streamerId||container&&container.dataset.streamerId;initialId&&setPlayerStreamer(initialId,void 0,activeCard)}}),window.StreamerFollow={toggle:toggle,handleClick:function handleClick(btn){if(btn)if("true"===btn.dataset.auth){var id=parseInt(btn.dataset.streamerId,10);id&&toggle(id,btn)}else showLogin()},setPlayerStreamer:setPlayerStreamer,getFollowedIds:function(){return followedIds},isFollowing:isFollowing,getStreamerPageUrl:getStreamerPageUrl,computeFollowRank:computeFollowRank,followHeartHtml:function followHeartHtml(){return'<span class="focus-card-follow-heart" aria-hidden="true"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg></span>'},refreshCardFollowRanks:refreshCardFollowRanks,openBrowseModal:openBrowseModal}}function labels(){var seeded=window.streamerFollowI18n||{};return{follow:seeded.follow||i18n_follow,unfollow:seeded.unfollow||i18n_unfollow,followed:seeded.followed||i18n_followed,unfollowed:seeded.unfollowed||i18n_unfollowed}}function showFollowerCount(){return!0===window.showFollowerCount}function followIconHtml(isFollowing){return'<i class="fa-solid '+(isF
1ollowing?"fa-heart-circle-minus":"fa-heart-circle-plus")+' streamer-follow-btn-icon" aria-hidden="true"></i>'}function t(key,fallback){var value=window.L&&window.L.T?window.L.T(key):null;return value&&value!==key?value:fallback}function followLabel(btn,isFollowing){var fromBtn=btn&&(isFollowing?btn.getAttribute("data-text-unfollow"):btn.getAttribute("data-text-follow"));if(fromBtn)return fromBtn;var pack=labels();return isFollowing?t("streamer.unfollow",pack.unfollow):t("streamer.follow",pack.follow)}function showLogin(){"function"!=typeof showLoginRequiredModal?window.AuthModal?window.AuthModal.open("login"):window.location.href="/Account/Login?returnUrl="+encodeURIComponent(window.location.pathname+window.location.search):showLoginRequiredModal()}function getStreamerPageUrl(streamerId){var id=parseInt(streamerId,10);return id>0?"/streamer/"+id:""}function isFollowing(streamerId){var id=parseInt(streamerId,10);return id>0&&followedIds.has(id)}function computeFollowRank(streamerId,isLive){var followed=isFollowing(streamerId);return isLive&&followed?0:isLive?1:followed?2:3}function refreshCardFollowRanks(){document.querySelectorAll(".focus-card-item").forEach(function(item){var card=item.querySelector(".focus-card, .beauty-card, .focus-match-card"),sid=card&&card.dataset?card.dataset.streamerId:"",isLive="true"===item.dataset.isLive||!!(card&&card.classList&&card.classList.contains("is-live")),followed=isFollowing(sid);item.dataset.followRank=String(computeFollowRank(sid,isLive)),item.classList.toggle("is-followed",followed),followed&&function ensureFollowHeart(item){var bg=item.querySelector(".focus-card-bg, .beauty-card-bg");bg&&!bg.querySelector(".focus-card-follow-heart")&&bg.insertAdjacentHTML("afterbegin",'<span class="focus-card-follow-heart" aria-hidden="true"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg></span>')}(item)}),"function"==typeof window.sortFocusCardsByTime&&window.sortFocusCardsByTime()}function applyButtonState(btn,isFollowing,followerCount){if(btn){btn.classList.toggle("is-following",!!isFollowing),btn.dataset.following=isFollowing?"true":"false";var nextLabel=followLabel(btn,isFollowing),label=btn.querySelector(".streamer-follow-fab-text, .streamer-follow-btn-text");label&&(label.textContent=btn.classList.contains("streamer-follow-header-btn")?followLabel(btn,!1):nextLabel);var icon=btn.querySelector(".streamer-follow-btn-icon");icon&&(icon.classList.toggle("fa-heart-circle-plus",!isFollowing),icon.classList.toggle("fa-heart-circle-minus",!!isFollowing)),(btn.classList.contains("is-icon-only")||btn.classList.contains("streamer-follow-header-btn"))&&(btn.setAttribute("aria-label",nextLabel),btn.setAttribute("title",nextLabel)),"number"==typeof followerCount&&document.querySelectorAll('.streamer-follower-count[data-streamer-id="'+btn.dataset.streamerId+'"]').forEach(function(el){el.textContent=String(followerCount)})}}function bindPlayerFollowButton(btn,id,following){if(btn){if(!id)return btn.hidden=!0,btn.classList.remove("is-visible"),void(btn.dataset.streamerId="");btn.hidden=!1,btn.classList.add("is-visible"),btn.dataset.streamerId=String(id),applyButtonState(btn,following)}}function setPlayerStreamer(streamerId,isFollowing,card){var fab=document.getElementById("streamerFollowFab"),headerBtn=document.getElementById("streamerFollowHeaderBtn"),container=document.getElementById("focusPlayerContainer")||document.getElementById("livePlayerContainer"),id=parseInt(streamerId,10)||0;container&&(container.dataset.streamerId=id?String(id):"");var following="boolean"==typeof isFollowing?isFollowing:followedIds.has(id);bindPlayerFollowButton(fab,id,following),bindPlayerFollowButton(headerBtn,id,following);var metaCard=card||document.querySelector(".focus-card.active, .beauty-card.active, .streamer-btn.active");if(fab&&metaCard){var meta=function resolveCardStreamerMeta(card){if(!card)return{name:"",avatar:""};
1var name=(card.dataset.streamer||card.dataset.streamerName||"").trim();if(!name){var item=card.closest(".focus-card-item")||card.parentElement||card,label=item.querySelector?item.querySelector(".focus-card-streamer"):null;name=(label&&label.textContent||"").replace(/\s+/g," ").trim()}return{name:name,avatar:(card.dataset.streamerAvatar||card.dataset.coverImage||"").trim()}}(metaCard);!function applyStreamerMeta(fab,name,avatar){if(fab){var nameEl=fab.querySelector(".streamer-follow-fab-name"),img=fab.querySelector(".streamer-follow-fab-avatar img"),displayName=function formatStreamerName(name){return"function"==typeof formatBlvStreamerName?formatBlvStreamerName(name||"")||name||"BLV":(name||"").trim()||"BLV"}(name);nameEl&&(nameEl.textContent=displayName,fab.dataset.streamerName=displayName),img&&(img.alt=displayName,avatar&&(img.src=avatar))}}(fab,meta.name,meta.avatar)}}function toggle(streamerId,sourceBtn){fetch("/profile/follows/toggle",{method:"POST",headers:{"Content-Type":"application/json"},credentials:"same-origin",body:JSON.stringify({streamerId:streamerId})}).then(function(res){return 401===res.status||403===res.status?(showLogin(),null):res.json()}).then(function(data){if(data)if(data.success){if(data.isFollowing?followedIds.add(streamerId):followedIds.delete(streamerId),function syncAllButtons(streamerId,isFollowing,followerCount){document.querySelectorAll('[data-streamer-id="'+streamerId+'"].streamer-follow-fab, [data-streamer-id="'+streamerId+'"].streamer-follow-btn').forEach(function(btn){applyButtonState(btn,isFollowing,followerCount)})}(streamerId,data.isFollowing,data.followerCount),refreshCardFollowRanks(),function updateFollowsPanel(streamerId,isFollowing,followerCount,sourceBtn){var list=document.getElementById("followsList");if(list){if(isFollowing){if(!list.querySelector('.follows-item[data-streamer-id="'+streamerId+'"]')){var card=sourceBtn&&sourceBtn.closest?sourceBtn.closest(".browse-streamer-card"):null;!function upsertFollowsRow(streamerId,name,avatar,followerCount){var list=document.getElementById("followsList");if(list)if(list.querySelector('.follows-item[data-streamer-id="'+streamerId+'"]'))syncFollowsEmptyState();else{list.insertAdjacentHTML("afterbegin",function buildFollowRow(streamerId,name,avatar,followerCount){var safeAvatar=avatar||"/images/default-avatar.svg",url=getStreamerPageUrl(streamerId);return'<div class="content-item follows-item" data-streamer-id="'+streamerId+'"><button type="button" class="follows-drag-handle" title="'+t("user.reorderFollows","Drag to reorder")+'"><svg viewBox="0 0 10 16" width="10" height="16" aria-hidden="true" focusable="false"><circle cx="2" cy="2" r="1.35"></circle><circle cx="8" cy="2" r="1.35"></circle><circle cx="2" cy="8" r="1.35"></circle><circle cx="8" cy="8" r="1.35"></circle><circle cx="2" cy="14" r="1.35"></circle><circle cx="8" cy="14" r="1.35"></circle></svg></button><div class="content-icon"><img src="'+safeAvatar+'" alt="" loading="lazy" draggable="false" onerror="this.onerror=null; this.src=\'/images/default-avatar.svg\';"></div><div class="content-body"><div class="content-title"></div>'+function followerCountHtml(streamerId,followerCount){return showFollowerCount()?'<div class="content-meta"><span><span class="streamer-follower-count" data-streamer-id="'+streamerId+'">'+(followerCount||0)+"</span> "+t("streamer.followers","followers")+"</span></div>":""}(streamerId,followerCount)+'</div><div class="content-actions"><a href="'+url+'" class="action-btn view" title="'+t("user.view","View")+'"><i class="fas fa-eye"></i></a><button type="button" class="action-btn delete streamer-follow-btn is-following" data-streamer-id="'+streamerId+'" data-auth="true" data-following="true" data-text-follow="'+t("streamer.follow",labels().follow)+'" data-text-unfollow="'+t("streamer.unfollow",labels().unfollow)+'" onclick="window.StreamerFollow?.handleClick(this)">'+followIconHtml(!0)+"</button></div></div>"}(streamerId,0,avatar,followerCount));var title=list.querySelector('.follows-item[data-streamer-id="'+streamerId+'"] .content-title');title&&(title.textContent=name||"BLV"),syncFollowsEmptyState()}}(streamerId,card&&card.dataset?card.dataset.name:"",card&&card.dataset?card.dataset.avatar:"",followerCount),updateFollowsCount(1)}}else list.querySelectorAll('.follows-item[data-streamer-id="'+streamerId+'"]').forEach(function(item){item.remove()}),updateFollowsCount(-1);syncFollowsEmptyState()}}(streamerId,data.isFollowing,data.followerCount,sourceBtn),"function"==typeof showToast){var pack=labels();showToast(data.isFollowing?t("streamer.followed",pack.followed):t("streamer.unfollowed",pack.unfollowed),"success")}}else data.message&&-1!==String(data.message).indexOf("未登录")?showLogin():"function"==typeof showToast&&showToast(data.message||"Failed","error")}).catch(function(){"function"==typeof showToast&&showToast("Network error","error")})}function syncFollowsEmptyState(){var list=document.getElementById("followsList"),empty=document.getElementById("followsEmpty");if(list&&empty){var has=list.querySelectorAll(".follows-item").length>0;empty.hidden=has,list.hidden=!has,window.ProfileListTools&&window.ProfileListTools.refresh("follows")}}function updateFollowsCount(delta){var countEl=document.querySelector('.profile-widget-card[data-tab="follows"] .profile-widget-value');countEl&&(countEl.textContent=String(Math.max(0,(parseInt(countEl.textContent,10)||0)+delta)))}function closeBrowseModal(){var modal=document.getElementById("browseStreamersModal");modal&&(modal.hidden=!0),document.body.style.overflow=""}function renderBrowseGrid(items){var grid=document.getElementById("browseStreamersGrid"),empty=document.getElementById("browseStreamersEmpty");
1if(grid&&empty){if(!items||!items.length)return grid.innerHTML="",void(empty.hidden=!1);empty.hidden=!0,grid.innerHTML=items.map(function(item){var following=!!item.isFollowing;return'<article class="browse-streamer-card" data-streamer-id="'+item.id+'" data-name="'+String(item.name||"").replace(/"/g,"&quot;")+'" data-avatar="'+String(item.avatarUrl||"").replace(/"/g,"&quot;")+'"><img src="'+(item.avatarUrl||"/images/default-avatar.svg")+'" alt="" loading="lazy" onerror="this.onerror=null; this.src=\'/images/default-avatar.svg\';"><div class="browse-streamer-name"></div>'+(showFollowerCount()?'<div class="browse-streamer-followers"><span class="streamer-follower-count" data-streamer-id="'+item.id+'">'+(item.followerCount||0)+"</span> "+t("streamer.followers","followers")+"</div>":"")+'<button type="button" class="streamer-follow-btn browse-follow-btn'+(following?" is-following":"")+'" data-streamer-id="'+item.id+'" data-auth="true" data-following="'+(following?"true":"false")+'" onclick="window.StreamerFollow?.handleClick(this)">'+followIconHtml(following)+'<span class="streamer-follow-btn-text">'+(following?t("streamer.unfollow",i18n_unfollow):t("streamer.follow",i18n_follow))+"</span></button></article>"}).join(""),Array.prototype.forEach.call(grid.querySelectorAll(".browse-streamer-card"),function(card,index){var nameEl=card.querySelector(".browse-streamer-name");nameEl&&(nameEl.textContent=items[index].name||"BLV")})}}function loadBrowseStreamers(){var grid=document.getElementById("browseStreamersGrid");if(grid){grid.innerHTML='<div class="browse-streamers-empty">'+t("match.player.loading","Loading...")+"</div>";var url="/profile/streamers/browse?page="+browseState.page+"&pageSize=300&q="+encodeURIComponent(browseState.q||"");fetch(url,{credentials:"same-origin"}).then(function(res){return res.json()}).then(function(data){data&&data.success?(browseState.total=data.total||0,renderBrowseGrid(data.items||[]),function renderBrowsePager(){var pager=document.getElementById("browseStreamersPager");if(pager){var pages=Math.ceil(browseState.total/300);if(browseState.total<=300)return pager.hidden=!0,void(pager.innerHTML="");pager.hidden=!1,pager.innerHTML='<button type="button" data-browse-page="prev"'+(browseState.page<=1?" disabled":"")+">"+t("user.prevPage","Previous")+"</button><span>"+browseState.page+" / "+pages+'</span><button type="button" data-browse-page="next"'+(browseState.page>=pages?" disabled":"")+">"+t("user.nextPage","Next")+"</button>"}}()):renderBrowseGrid([])}).catch(function(){renderBrowseGrid([])})}}function openBrowseModal(){var modal=document.getElementById("browseStreamersModal");if(modal){modal.hidden=!1,document.body.style.overflow="hidden",browseState.page=1;var input=document.getElementById("browseStreamersSearch");input&&!input.dataset.bound&&(input.dataset.bound="true",input.addEventListener("input",function(){clearTimeout(browseState.timer),browseState.timer=setTimeout(function(){browseState.q=input.value||"",browseState.page=1,loadBrowseStreamers()},250)})),input&&(browseState.q=input.value||"",input.focus()),loadBrowseStreamers()}}function initStreamerPagedList(root){if(root){var pageSize=parseInt(root.dataset.pageSize,10)||20,pager=root.querySelector("[data-list-pager]"),empty=root.querySelector("[data-schedule-empty]"),buttons=root.querySelectorAll("[data-schedule-filter]"),page=1;buttons.forEach(function(btn){btn.addEventListener("click",function(){applyFilter(btn.dataset.scheduleFilter)})}),buttons.length?applyFilter("all"):render()}function applyFilter(filter){root.querySelectorAll("[data-schedule-row]").forEach(function(row){var sport=(row.dataset.sport||"football").toLowerCase(),live="true"===row.dataset.live,show=!filter||"all"===filter||"live"===filter&&live||"football"===filter&&"football"===sport||"basketball"===filter&&"basketball"===sport;row.dataset.filterHidden=show?"false":"true"}),buttons.forEach(function(btn){btn.classList.toggle("is-active",btn.dataset.scheduleFilter===filter)}),page=1,render()}function render(){var rows=Array.from(root.querySelectorAll("[data-page-row]")),eligible=function eligibleRows(){return Array.from(root.querySelectorAll("[data-page-row]")).filter(function(row){return"true"!==row.dataset.f
1ilterHidden})}(),total=eligible.length,pages=Math.max(1,Math.ceil(total/pageSize)||1);page>pages&&(page=pages);var showPager=total>pageSize;eligible.forEach(function(row,i){var start=(page-1)*pageSize;row.hidden=showPager&&(i<start||i>=start+pageSize)}),rows.forEach(function(row){"true"===row.dataset.filterHidden&&(row.hidden=!0)}),empty&&(empty.hidden=total>0),function renderPager(pages,current){if(pager)if(pager.innerHTML="",pages<=1)pager.hidden=!0;else{pager.hidden=!1,addBtn("«",1,1===current,!1),addBtn("‹",current-1,1===current,!1);for(var start=Math.max(1,current-2),end=Math.min(pages,start+4),i=start=Math.max(1,end-4);i<=end;i++)addBtn(String(i),i,!1,i===current);addBtn("›",current+1,current===pages,!1),addBtn("»",pages,current===pages,!1)}function addBtn(label,target,disabled,active){var btn=document.createElement("button");btn.type="button",btn.className="streamer-pager-btn"+(active?" is-active":""),btn.textContent=label,btn.disabled=!!disabled,disabled||active||btn.addEventListener("click",function(){page=target,render()}),pager.appendChild(btn)}}(showPager?pages:1,page)}}}(),function(){"use strict";let modalEl=null,turnstileWidgets={login:null,register:null};const T=(key,fallback)=>{if(window.i18n?.isReady?.()&&"function"==typeof window.i18n.t){const v=window.i18n.t(key);if(v&&v!==key)return v}else if(window.L?.T){const v=window.L.T(key);if(v&&v!==key)return v}return fallback||key};function isCaptchaEnabled(){const meta=document.querySelector('meta[name="captcha-enabled"]');return!meta||"false"!==meta.content}function appendDeviceId(formData){const deviceId=function getDeviceId(){if("function"==typeof window.getChatVisitorId)return window.getChatVisitorId();if("function"==typeof window.getVisitorId)return window.getVisitorId();if(window.SiteTracker&&"function"==typeof window.SiteTracker.getVisitorId)return window.SiteTracker.getVisitorId();const key="visitor_id";let stored=localStorage.getItem(key);return stored||(stored="v_"+Date.now().toString(36)+Math.random().toString(36).slice(2,11),localStorage.setItem(key,stored),stored)}();return deviceId&&formData.append("DeviceId",deviceId),formData}function buildModal(){const token=function getToken(){const el=document.querySelector('input[name="__RequestVerificationToken"]');return el?el.value:""}(),returnUrl=window.location.pathname+window.location.search,div=(document.querySelector('meta[name="turnstile-sitekey"]'),document.createElement("div"));return div.id="authModal",div.className="auth-modal-overlay",div.innerHTML=`\n<div class="auth-modal-container">\n  <button class="auth-modal-close" id="authModalClose" aria-label="Đóng"><i class="fas fa-times"></i></button>\n  \n  <div class="auth-modal-tabs">\n    <button class="auth-modal-tab active" data-tab="login">${T("auth.loginTitle","Đăng nhập")}</button>\n    <button class="auth-modal-tab" data-tab="register">${T("auth.registerTitle","Đăng ký")}</button>\n    <div class="auth-modal-tab-indicator"></div>\n  </div>\n  \n  <div class="auth-modal-panel active" id="authPanelLogin">\n    \x3c!-- Google 登录优先 --\x3e\n    <form method="post" action="/Account/ExternalLogin" class="auth-modal-social-top">\n      <input type="hidden" name="__RequestVerificationToken" value="${token}" />\n      <input type="hidden" name="provider" value="Google" />\n      <input type="hidden" name="returnUrl" value="${returnUrl}" />\n      <button type="submit" class="auth-modal-google-btn">\n        <svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path></svg>
1\n        <span>${T("auth.loginWithGoogle","Đăng nhập bằng Google")}</span>\n      </button>\n    </form>\n    \n    <div class="auth-modal-divider"><span>${T("auth.or","hoặc")}</span></div>\n    \n    <div class="auth-modal-error d-none" id="loginError"></div>\n    <form id="authLoginForm" autocomplete="on">\n      <input type="hidden" name="__RequestVerificationToken" value="${token}" />\n      <div class="auth-modal-field">\n        <label>\n          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>\n          ${T("auth.username","Tên đăng nhập")}\n        </label>\n        <input type="text" name="Username" autocomplete="username" required data-i18n-placeholder="auth.usernamePlaceholder" />\n      </div>\n      <div class="auth-modal-field">\n        <label>\n          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>\n          ${T("auth.password","Mật khẩu")}\n        </label>\n        <input type="password" name="Password" autocomplete="current-password" required data-i18n-placeholder="auth.passwordPlaceholder" />\n      </div>\n      <div class="auth-modal-row">\n        <label class="auth-modal-check"><input type="checkbox" name="RememberMe" value="true" /> ${T("auth.rememberMe","Ghi nhớ đăng nhập")}</label>\n      </div>\n      <div class="auth-modal-turnstile turnstile-container" id="loginTurnstile" data-theme="dark"></div>\n      <button type="submit" class="auth-modal-submit" id="loginSubmitBtn" disabled>${T("auth.loginButton","Đăng nhập")}</button>\n    </form>\n  </div>\n  \n  <div class="auth-modal-panel" id="authPanelRegister">\n    \x3c!-- Google 注册优先 --\x3e\n    <form method="post" action="/Account/ExternalLogin" class="auth-modal-social-top">\n      <input type="hidden" name="__RequestVerificationToken" value="${token}" />\n      <input type="hidden" name="provider" value="Google" />\n      <input type="hidden" name="returnUrl" value="${returnUrl}" />\n      <button type="submit" class="auth-modal-google-btn">\n        <svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path></svg>\n        <span>${T("auth.registerWithGoogle","Đăng ký bằng Google")}</span>\n      </button>\n    </form>\n    \n    <div class="auth-modal-divider"><span>${T("auth.or","hoặc")}</span></div>\n    \n    <div class="auth-modal-error d-none" id="registerError"></div>\n    <form id="authRegisterForm" autocomplete="on">\n      <input type="hidden" name="__RequestVerificationToken" value="${token}" />\n      <div class="auth-modal-field">\n        <label>\n          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>\n          ${T("auth.username","Tên đăng nhập")}\n        </label>\n        <input type="text" name="Username" autocomplete="username" required minlength="3" maxlength="50" data-i18n-placeholder="auth.usernamePlaceholder" />\n      </div>\n      <div class="auth-modal-field">\n        <label>\n          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>\n          ${T("auth.nickname","Biệt danh")}\n        </label>\n        <input type="text" name="Nickname" autocomplete="nickname" required minlength="2" maxlength="50" data-i18n-placeholder="auth.nicknamePlaceholder" />\n      </div>\n      <div class="auth-modal-field">\n        <label>\n          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>\n          ${T("auth.password","Mật khẩu")}\n        </label>\n        <input type="password" name="Password" autocomplete="new-password" required minlength="6" data-i18n-placeholder="auth.passwordPlaceholder" />\n      </div>\n      <div class="auth-modal-field">\n        <label>\n          <svg viewBox="0 0 24 24" fill="currentColor">
1<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>\n          ${T("auth.confirmPassword","Xác nhận mật khẩu")}\n        </label>\n        <input type="password" name="ConfirmPassword" autocomplete="new-password" required minlength="6" data-i18n-placeholder="auth.confirmPasswordPlaceholder" />\n      </div>\n      <div class="auth-modal-turnstile turnstile-container" id="registerTurnstile" data-theme="dark"></div>\n      <button type="submit" class="auth-modal-submit" id="registerSubmitBtn" disabled>${T("auth.registerButton","Đăng ký")}</button>\n    </form>\n  </div>\n</div>`,div}function placeholderText(key,fallback){const value=T(key,fallback);return value&&value!==key?value:fallback}function setPlaceholders(){if(!modalEl)return;const usernamePh=placeholderText("auth.usernamePlaceholder","Enter username"),passwordPh=placeholderText("auth.passwordPlaceholder","Enter password"),nicknamePh=placeholderText("auth.nicknamePlaceholder","Enter nickname"),confirmPh=placeholderText("auth.confirmPasswordPlaceholder","Re-enter password"),loginUsername=modalEl.querySelector('#authPanelLogin input[name="Username"]'),loginPassword=modalEl.querySelector('#authPanelLogin input[name="Password"]');loginUsername&&(loginUsername.placeholder=usernamePh),loginPassword&&(loginPassword.placeholder=passwordPh);const regUsername=modalEl.querySelector('#authPanelRegister input[name="Username"]'),regNickname=modalEl.querySelector('#authPanelRegister input[name="Nickname"]'),regPassword=modalEl.querySelector('#authPanelRegister input[name="Password"]'),regConfirm=modalEl.querySelector('#authPanelRegister input[name="ConfirmPassword"]');regUsername&&(regUsername.placeholder=usernamePh),regNickname&&(regNickname.placeholder=nicknamePh),regPassword&&(regPassword.placeholder=passwordPh),regConfirm&&(regConfirm.placeholder=confirmPh)}function openAuthModal(tab){modalEl?(modalEl.style.display="",setPlaceholders()):(modalEl=buildModal(),document.body.appendChild(modalEl),bindEvents(),setPlaceholders(),initCaptcha()),/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)&&(window._authModalScrollY=window.scrollY,document.body.style.top=`-${window._authModalScrollY}px`),switchTab(tab||"login"),function pauseBackgroundMedia(){const paused=[];document.querySelectorAll("video").forEach(video=>{if(!video.paused&&!video.ended)try{video.pause(),paused.push(video)}catch(_){}}),window._authModalPausedVideos=paused}(),modalEl.classList.add("show"),document.body.classList.add("auth-modal-open"),/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)||setTimeout(()=>{const panel=modalEl.querySelector(".auth-modal-panel.active"),input=panel?.querySelector('input:not([type="hidden"])');input&&input.focus()},200)}function closeAuthModal(){modalEl&&(modalEl.classList.remove("show"),document.body.classList.remove("auth-modal-open"),function resumeBackgroun
1dMedia(){(window._authModalPausedVideos||[]).forEach(video=>{try{const playPromise=video.play();playPromise&&"function"==typeof playPromise.catch&&playPromise.catch(()=>{})}catch(_){}}),window._authModalPausedVideos=[]}(),void 0!==window._authModalScrollY&&(document.body.style.top="",window.scrollTo(0,window._authModalScrollY),delete window._authModalScrollY))}function switchTab(tab){if(!modalEl)return;modalEl.querySelectorAll(".auth-modal-tab").forEach(t=>t.classList.toggle("active",t.dataset.tab===tab)),document.getElementById("authPanelLogin").classList.toggle("active","login"===tab),document.getElementById("authPanelRegister").classList.toggle("active","register"===tab);const indicator=modalEl.querySelector(".auth-modal-tab-indicator");indicator&&(indicator.style.transform="register"===tab?"translateX(100%)":"translateX(0)"),modalEl.querySelectorAll(".auth-modal-error").forEach(e=>{e.classList.add("d-none"),e.textContent=""})}function showError(id,msg){const el=document.getElementById(id);el&&(el.textContent=msg,el.classList.remove("d-none"))}function setLoading(btn,loading,label){btn&&(btn.disabled=loading,btn.innerHTML=loading?'<span class="spinner-border spinner-border-sm me-2"></span>'+label+"...":label)}function initCaptcha(){if(!window.CaptchaManager)return void console.error("[AuthModal] CaptchaManager 未加载");if(!isCaptchaEnabled())return document.querySelectorAll(".turnstile-container").forEach(el=>{el.style.display="none"}),document.getElementById("loginSubmitBtn").disabled=!1,void(document.getElementById("registerSubmitBtn").disabled=!1);if("recaptcha"===CaptchaManager.provider)return document.querySelectorAll(".turnstile-container").forEach(el=>{el.style.display="none"}),document.getElementById("loginSubmitBtn").disabled=!1,void(document.getElementById("registerSubmitBtn").disabled=!1);if(void 0===window.turnstile){const retryCount=window._captchaRetryCount||0;return retryCount<50?(window._captchaRetryCount=retryCount+1,void setTimeout(initCaptcha,100)):void console.error("[AuthModal] Turnstile API 加载超时")}const loginContainer=document.getElementById("loginTurnstile"),registerContainer=document.getElementById("registerTurnstile");loginContainer&&CaptchaManager.render(loginContainer,"login",token=>{document.getElementById("loginSubmitBtn").disabled=!1}),registerContainer&&CaptchaManager.render(registerContainer,"register",token=>{document.getElementById("registerSubmitBtn").disabled=!1})}function bindEvents(){modalEl.querySelector("#authModalClose").addEventListener("click",closeAuthModal),modalEl.addEventListener("click",e=>{e.target===modalEl&&closeAuthModal()}),document.addEventListener("keydown",e=>{"Escape"===e.key&&closeAuthModal()}),modalEl.querySelectorAll(".auth-modal-tab").forEach(t=>{t.addEventListener("click",()=>switchTab(t.dataset.tab))}),modalEl.querySelector("#authLoginForm").addEventListener("submit",async e=>{e.preventDefault();const btn=document.getElementById("loginSubmitBtn");if(btn.disabled)return;const form=e.target;if(isCaptchaEnabled()){const captchaToken=await window.CaptchaManager.getToken("login");if(!captchaToken)return void showError("loginError",T("auth.captchaRequired","Vui lòng hoàn thành xác minh"));window.CaptchaManager.addTokenToForm(form,captchaToken)}const label=T("auth.loginButton","Đăng nhập");setLoading(btn,!0,label),document.getElementById("loginError")?.classList.add("d-none");try{const formData=new FormData(form);appendDeviceId(formData);const resp=await fetch("/Account/AjaxLogin",{method:"POST",body:formData}),data=await resp.json();data.succeeded?(btn.innerHTML='<i class="fas fa-check me-2"></i>'+T("auth.loginSuccess","Đăng nhập thành công"),btn.style.background="linear-gradient(135deg, #10b981, #34d399)",window.dispatchEvent(new CustomEvent("userLoggedIn")),setTimeout(()=>{closeAuthModal(),window.location.reload()},600)):(showError("loginError",data.message||T("auth.loginFailed","Đăng nhập thất bại")),window.CaptchaManager.reset("login"),setLoading(btn,!1,label))}catch(error){console.error("登录请求失败:",error),showError("loginError",T("auth.networkError","Lỗi mạng, vui lòng thử lại")),window.CaptchaManager.reset("login"),setLoading(btn,!1,label)}}),modalEl.querySelector("#authRegisterForm").addEventListener("submit",async e=>{e.preventDefault();const form=e.target;if(form.querySelector('input[name="Password"]').value!==form.querySelector('input[name="ConfirmPassword"]').value)return void showError("registerError",T("auth.validation.passwordMismatch","Mật khẩu không khớp"));if(isCaptchaEnabled()){const captchaToken=await window.CaptchaManager.getToken("register");if(!captchaToken)return void showError("registerError",T("auth.captchaRequired","Vui lòng hoàn thành xác minh"));window.CaptchaManager.addTokenToForm(form,captchaToken)}const btn=document.getElementById("registerSubmitBtn"),label=T("auth.registerButton","Đăng ký");setLoading(btn,!0,label),document.getElementById("registerError")?.classList.add("d-none");try{const formData=new FormData(form);appendDeviceId(formData);const resp=await fetch("/Account/AjaxRegister",{method:"POST",body:formData}),data=await resp.json();data.succeeded?(btn.innerHTML='<i class="fas fa-check me-2"></i>'+T("auth.registerSuccess","Đăng ký thành công"),btn.style.background="linear-gradient(135deg, #10b981, #34d399)",window.dispatchEvent(new CustomEvent("userLoggedIn")),setTimeout(()=>{closeAuthModal(),window.location.reload()},600)):(showError("registerError",data.message||T("auth.registerFailed","Đăng ký thất bại")),window.CaptchaManager.reset("register"),setLoading(btn,!1,label))}catch(error){console.error("注册请求失败:",error),showError("registerError",T("auth.networkError","Lỗi mạng, vui lòng thử lại")),window.CaptchaManager.reset("register"),setLoading(btn,!1,label)}})}
1function invalidateModal(){if(!modalEl)return;const wasOpen=modalEl.classList.contains("show"),tab=modalEl.querySelector(".auth-modal-tab.active")?.dataset?.tab||"login";modalEl.remove(),modalEl=null,turnstileWidgets={login:null,register:null},wasOpen&&openAuthModal(tab)}function schedulePreload(){let scheduled=!1;const run=()=>{scheduled||(scheduled=!0,setTimeout(()=>{window.AuthModal&&!document.body.classList.contains("authenticated")&&window.AuthModal.preload()},2e3))};window.i18n?.isReady?.()?run():(window.addEventListener("i18nReady",run,{once:!0}),setTimeout(run,4e3))}window.addEventListener("i18nReady",()=>{setPlaceholders(),invalidateModal()}),window.addEventListener("langChange",()=>{setPlaceholders(),invalidateModal()}),window.AuthModal={open:openAuthModal,close:closeAuthModal,preload:function(){modalEl||(modalEl=buildModal(),modalEl.style.display="none",document.body.appendChild(modalEl),bindEvents(),setPlaceholders(),initCaptcha())}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",function(){window.turnstile,schedulePreload()}):(window.turnstile,schedulePreload())}(),function(){"use strict";let chatSettings=null,canMuteUsers=!1,isVip=!1,lastMessageTime=0,messageTimestamps=[];async function muteUser(userName,duration,reason){try{const response=await fetch("/api/chat/mute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userName:userName,duration:duration,reason:reason})}),data=await response.json();response.ok?showChatNotification(data.message,"success"):showChatNotification(data.error||(window.L?.T?window.L.T("match.chat.muteFailed"):"禁言失败"),"error")}catch(error){console.error("Mute failed:",error),showChatNotification(window.L?.T?window.L.T("match.chat.muteFailedRetry"):"禁言失败,请稍后重试","error")}}function showChatNotification(message,type="info"){const notification=document.createElement("div");notification.className=`chat-notification chat-notification-${type}`,notification.innerHTML=`\n            <i class="fas fa-${"success"===type?"check-circle":"error"===type?"exclamation-circle":"info-circle"}"></i>\n            <span>${message}</span>\n        `,(document.querySelector(".chat-room-container")||document.body).appendChild(notification),setTimeout(()=>{notification.classList.add("fade-out"),setTimeout(()=>notification.remove(),300)},3e3)}async function init(){!function addStyles(){const style=document.createElement("style");style.textContent="\n            .chat-msg-actions {\n                position: absolute;\n                right: 8px;\n                top: 50%;\n                transform: translateY(-50%);\n                opacity: 0;\n                transition: opacity 0.2s;\n            }\n\n            .chat-message:hover .chat-msg-actions {\n                opacity: 1;\n            }\n\n            .chat-mute-btn {\n                background: rgba(239, 68, 68, 0.8);\n                border: none;\n                border-radius: 4px;\n                color: white;\n                padding: 4px 8px;\n                cursor: pointer;\n                font-size: 12px;\n                transition: background 0.2s;\n            }\n\n            .chat-mute-btn:hover {\n                background: rgba(239, 68, 68, 1);\n            }\n\n            .chat-mute-dialog {\n                position: fixed;\n                top: 0;\n                left: 0;\n                right: 0;\n                bottom: 0;\n                background: rgba(0, 0, 0, 0.6);\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                z-index: 10000;\n            }\n\n            .chat-mute-dialog-content {\n                background: #1e293b;\n                border-radius: 12px;\n                padding: 24px;\n                min-width: 320px;\n                color: #e2e8f0;\n            }\n\n            .chat-mute-dialog-content h4 {\n                margin: 0 0 16px;\n                color: #f87171;\n            }\n\n            .mute-duration-options {\n                display: flex;\n                flex-direction: column;\n                gap: 8px;\n                margin: 16px 0;\n            }\n\n            .mute-duration-options label {\n                display: flex;\n                align-items: center;\n                gap: 8px;\n                cursor: pointer;\n            }\n\n            .mute-reason input {\n                width: 100%;\n                padding: 8px 12px;\n                background: rgba(15, 23, 42, 0.8);
1\n                border: 1px solid rgba(148, 163, 184, 0.3);\n                border-radius: 6px;\n                color: #e2e8f0;\n            }\n\n            .mute-dialog-actions {\n                display: flex;\n                gap: 12px;\n                margin-top: 16px;\n                justify-content: flex-end;\n            }\n\n            .mute-dialog-actions button {\n                padding: 8px 16px;\n                border-radius: 6px;\n                border: none;\n                cursor: pointer;\n                font-weight: 500;\n            }\n\n            .mute-dialog-actions .btn-cancel {\n                background: #475569;\n                color: #e2e8f0;\n            }\n\n            .mute-dialog-actions .btn-confirm {\n                background: #ef4444;\n                color: white;\n            }\n\n            .chat-notification {\n                position: absolute;\n                bottom: 80px;\n                left: 50%;\n                transform: translateX(-50%);\n                padding: 10px 20px;\n                border-radius: 8px;\n                display: flex;\n                align-items: center;\n                gap: 8px;\n                z-index: 1000;\n                animation: slideUp 0.3s ease;\n            }\n\n            .chat-notification-success {\n                background: rgba(34, 197, 94, 0.9);\n                color: white;\n            }\n\n            .chat-notification-error {\n                background: rgba(239, 68, 68, 0.9);\n                color: white;\n            }\n\n            .chat-notification-info {\n                background: rgba(59, 130, 246, 0.9);\n                color: white;\n            }\n\n            .chat-notification.fade-out {\n                opacity: 0;\n                transition: opacity 0.3s;\n            }\n\n            @keyframes slideUp {\n                from {\n                    opacity: 0;\n                    transform: translateX(-50%) translateY(20px);\n                }\n                to {\n                    opacity: 1;\n                    transform: translateX(-50%) translateY(0);\n                }\n            }\n\n            /* Settings indicator */\n            .chat-settings-indicator {\n                font-size: 11px;\n                color: #94a3b8;\n                padding: 4px 12px;\n                border-top: 1px solid rgba(148, 163, 184, 0.1);\n                background: rgba(15, 23, 42, 0.5);\n            }\n        ",document.head.appendChild(style)}(),await async function loadChatSettings(){try{const response=await fetch("/api/chat/settings");response.ok&&(chatSettings=await response.json())}catch(error){console.error("Failed to load chat settings:",error)}}(),await async function checkMutePermission(){try{const response=await fetch("/api/chat/can-mute");if(response.ok){const data=await response.json();canMuteUsers=data.canMute,isVip=data.isVip}}catch(error){console.error("Failed to check mute permission:",error)}}();const chatMessages=document.querySelector(".chat-messages, .chat-room-messages");chatMessages&&canMuteUsers&&new MutationObserver(mutations=>{mutations.forEach(mutation=>{mutation.addedNodes.forEach(node=>{if(1===node.nodeType&&node.classList.contains("chat-message")){const userName=node.dataset.username||node.querySelector(".chat-username, .chat-user-name")?.textContent;userName&&function addMuteButtonToMessage(messageElement,userName){if(!canMuteUsers||!userName||"System"===userName)return;if(messageElement.querySelector(".chat-mute-btn"))return;const muteBtn=document.createElement("button");muteBtn.className="chat-mute-btn",muteBtn.innerHTML='<i class="fas fa-volume-mute"></i>',muteBtn.title=window.L?.T?window.L.T("match.chat.muteUser",{userName:userName}):`禁言 ${userName}`,muteBtn.onclick=e=>{e.stopPropagation(),function showMuteDialog(userName){const dialog=document.createElement("div");dialog.className="chat-mute-dialog",dialog.innerHTML=`\n            <div class="chat-mute-dialog-content">\n                <h4><i class="fas fa-volume-mute"></i> ${window.L?.T?window.L.T("match.chat.muteDialogTitle"):"禁言用户"}</h4>\n                <p>${window.L?.T?window.L.T("match.chat.confirmMuteMessage",{userName:userName}):`确定要禁言 <strong>${userName}</strong>
1 吗?`}</p>\n                <div class="mute-duration-options">\n                    <label><input type="radio" name="muteDuration" value="5" checked> ${window.L?.T?window.L.T("match.chat.muteDuration.5min"):"5分钟"}</label>\n                    <label><input type="radio" name="muteDuration" value="15"> ${window.L?.T?window.L.T("match.chat.muteDuration.15min"):"15分钟"}</label>\n                    <label><input type="radio" name="muteDuration" value="60"> ${window.L?.T?window.L.T("match.chat.muteDuration.1hour"):"1小时"}</label>\n                    <label><input type="radio" name="muteDuration" value="1440"> ${window.L?.T?window.L.T("match.chat.muteDuration.24hours"):"24小时"}</label>\n                    <label><input type="radio" name="muteDuration" value="0"> ${window.L?.T?window.L.T("match.chat.muteDuration.permanent"):"永久"}</label>\n                </div>\n                <div class="mute-reason">\n                    <input type="text" placeholder="${window.L?.T?window.L.T("match.chat.muteReason"):"禁言原因(可选)"}" id="muteReason" />\n                </div>\n                <div class="mute-dialog-actions">\n                    <button class="btn-cancel">${window.L?.T?window.L.T("common.cancel"):"取消"}</button>\n                    <button class="btn-confirm">${window.L?.T?window.L.T("match.chat.confirmMute"):"确认禁言"}</button>\n                </div>\n            </div>\n        `,document.body.appendChild(dialog),dialog.querySelector(".btn-cancel").onclick=()=>dialog.remove(),dialog.querySelector(".btn-confirm").onclick=async()=>{const duration=dialog.querySelector('input[name="muteDuration"]:checked').value,reason=dialog.querySelector("#muteReason").value;await muteUser(userName,parseInt(duration),reason),dialog.remove()},dialog.onclick=e=>{e.target===dialog&&dialog.remove()}}(userName)};let actionsContainer=messageElement.querySelector(".chat-msg-actions");actionsContainer||(actionsContainer=document.createElement("div"),actionsContainer.className="chat-msg-actions",messageElement.appendChild(actionsContainer)),actionsContainer.appendChild(muteBtn)}(node,userName)}})})}).observe(chatMessages,{childList:!0})}window.ChatEnhanced={validateMessageSend:function validateMessageSend(){if(!chatSettings)return{valid:!0};const now=Date.now(),elapsedSeconds=(now-lastMessageTime)/1e3;if(elapsedSeconds<chatSettings.minIntervalSeconds){const remaining=Math.ceil(chatSettings.minIntervalSeconds-elapsedSeconds);return{valid:!1,message:window.L?.T?window.L.T("match.chat.sendTooFrequent",{remaining:remaining}):`发送过于频繁,请等待 ${remaining} 秒后再试`}}const oneMinuteAgo=now-6e4;return messageTimestamps=messageTimestamps.filter(t=>t>oneMinuteAgo),messageTimestamps.length>=chatSettings.maxMessagesPerMinute?{valid:!1,message:window.L?.T?window.L.T("match.chat.maxMessagesPerMinute",{count:chatSettings.maxMessagesPerMinute}):`每分钟最多发送 ${chatSettings.maxMessagesPerMinute} 条消息,请稍后再试`}:{valid:!0}},recordMessageSent:function recordMessageSent(){const now=Date.now();lastMessageTime=now,messageTimestamps.push(now)},showChatNotification:showChatNotification,muteUser:muteUser,unmuteUser:async function unmuteUser(userName){try{const response=await fetch("/api/chat/unmute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userName:userName})}),data=await response.json();response.ok?showChatNotification(data.message,"success"):showChatNotification(data.error||(window.L?.T?window.L.T("match.chat.unmuteFailed"):"解除禁言失败"),"error")}catch(error){console.error("Unmute failed:",error),showChatNotification(window.L?.T?window.L.T("match.chat.unmuteFailedRetry"):"解除禁言失败,请稍后重试","error")}},getSettings:()=>chatSettings,canMute:()=>canMuteUsers,isVip:()=>isVip},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",init):init()}(),function(){"use strict";const TIER_ICONS=["/images/vip/bronze.svg","/images/vip/silver.svg","/images/vip/gold.svg","/images/vip/platinum.svg","/images/vip/diamond.svg","/images/vip/legend.svg"],SUB_NAMES=["I","II","III"],TIER_EXP_START=[0,150,800,2500,7e3,16e3];let _i18n=null;function getTierInfo(level){level<1&&(level=1),level>18&&(level=18);const tierIndex=Math.floor((level-1)/3),subIndex=(level-1)%3;return{tierIndex:tierIndex,subIndex:subIndex,icon:TIER_ICONS[tierIndex],subN
1ame:SUB_NAMES[subIndex]}}function tierName(tierIndex){return _i18n&&_i18n.tierNames&&_i18n.tierNames[tierIndex]?_i18n.tierNames[tierIndex]:["Bronze","Silver","Gold","Platinum","Diamond","Legend"][tierIndex]||""}function t(key,fallback){return _i18n&&_i18n[key]||fallback||key}function getBarPercent(vipData){const nextExp=Number(vipData.nextLevelExp),total=Number(vipData.totalExp)||0;return nextExp?Math.min(100,Math.max(0,total/nextExp*100)):100}async function fetchExpHistory(params){const query=new URLSearchParams;params.type&&query.set("type",params.type),params.start&&query.set("start",params.start),params.end&&query.set("end",params.end),query.set("page",String(params.page||1)),query.set("pageSize",String(params.pageSize||20));try{const resp=await fetch("/Account/GetExpHistory?"+query.toString());if(!resp.ok)return null;const result=await resp.json();return result.success?(result.i18n&&(_i18n=result.i18n),result):null}catch{return null}}function expHistoryLabel(key,fallback,params){const fromPage=window.L?.T?window.L.T("user.vip."+key,params||{}):null;if(fromPage&&fromPage!=="user.vip."+key)return fromPage;let text=t(key,fallback);return params&&(text=text.replace(/\{(\w+)\}/g,(match,name)=>void 0!==params[name]?String(params[name]):match)),text}window.VipLevel={getTierInfo:getTierInfo,renderBadgeHtml:function renderBadgeHtml(level,levelName,isAdmin,isStreamer){if(isAdmin)return'<span class="vip-badge admin-badge" title="Admin"><span class="vip-badge-dot admin-dot" aria-hidden="true"></span><span class="vip-badge-level">Admin</span></span>';if(isStreamer)return'<span class="vip-badge streamer-badge" title="Streamer"><span class="vip-badge-dot streamer-dot" aria-hidden="true"></span><span class="vip-badge-level">Streamer</span></span>';(!level||level<1)&&(level=1);const info=getTierInfo(level),title=levelName||`${tierName(info.tierIndex)} ${info.subName}`;return`<span class="vip-badge vip-tier-${info.tierIndex} vip-sub-${info.subIndex}" data-vip-level="${level}" title="${title}"><span class="vip-badge-dot" aria-hidden="true"></span><span class="vip-badge-level">Lv.${level}</span></span>`},renderPopupVipHtml:function renderPopupVipHtml(vipData,i18n){if(!vipData)return"";i18n&&(_i18n=i18n);const level=vipData.level||1,info=getTierInfo(level),name=vipData.levelName||`${tierName(info.tierIndex)} ${info.subName}`,nextExp=vipData.nextLevelExp,progress=getBarPercent(vipData),expLabel=t("exp","EXP"),expMaxLabel=t("expMax","MAX"),expText=nextExp?`${vipData.totalExp} / ${nextExp}`:`${vipData.totalExp} (${expMaxLabel})`,regText=vipData.registeredAt||"";return`\n            <div class="popup-vip-card tier-${info.tierIndex}">\n                <div class="popup-vip-card-bg"></div>\n                <div class="popup-vip-card-content">\n                    <div class="popup-vip-rank">\n                        <img class="popup-vip-rank-icon" src="${info.icon}" alt="">\n                        <div class="popup-vip-rank-info">\n                            <span class="popup-vip-rank-name">${name}</span>\n                            <span class="popup-vip-rank-level">Lv.${level}</span>\n                        </div>\n                    </div>\n                    <div class="popup-vip-progress-wrap">\n                        <div class="popup-vip-progress-label">\n                            <span>${expLabel}</span><span>${expText}</span>\n                        </div>\n                        <div class="popup-vip-progress-track">\n                            <div class="popup-vip-progress-fill tier-fill-${info.tierIndex}" style="width:${progress}%"></div>\n                        </div>\n                    </div>\n                    <div class="popup-vip-stats-row">\n                        <div class="popup-vip-stat-item">\n                            <span class="popup-vip-stat-val">${vipData.consecutiveActiveDays||0}</span>\n                            <span class="popup-vip-stat-lbl">${t("consecutiveActive","Streak")}</span>\n                        </div>\n                        <div class="popup-vip-stat-item">
1\n                            <span class="popup-vip-stat-val">${vipData.totalActiveDays||0}</span>\n                            <span class="popup-vip-stat-lbl">${t("totalActive","Active")}</span>\n                        </div>\n                        <div class="popup-vip-stat-item">\n                            <span class="popup-vip-stat-val">${vipData.totalMessageCount||0}</span>\n                            <span class="popup-vip-stat-lbl">${t("totalMessages","Messages")}</span>\n                        </div>\n                    </div>\n                    ${regText?`<div class="popup-vip-reg"><i class="fas fa-calendar-alt"></i> ${t("registeredAt","Joined")} ${regText}</div>`:""}\n                </div>\n            </div>`},renderProfileVipCard:function renderProfileVipCard(vipData,i18n){if(!vipData)return"";i18n&&(_i18n=i18n);const level=vipData.level||1,info=getTierInfo(level),name=vipData.levelName||`${tierName(info.tierIndex)} ${info.subName}`,nextExp=vipData.nextLevelExp,progress=getBarPercent(vipData),expMaxLabel=t("expMax","MAX"),progressText=nextExp?`${vipData.totalExp} / ${nextExp}`:`${vipData.totalExp} (${expMaxLabel})`,daysUnit=t("days","d"),msgsUnit=t("messages","msg");let tierDots="";for(let i=0;i<6;i++)tierDots+=`<div class="profile-tier-dot ${i<=info.tierIndex?"active":""} ${i===info.tierIndex?"current":""}" title="${tierName(i)}"></div>`;let nextTierHtml="";if(info.tierIndex<5){const nextTierIdx=info.tierIndex+1,nextTierExp=TIER_EXP_START[nextTierIdx],remaining=Math.max(0,nextTierExp-vipData.totalExp),hint=t("nextTierHint","{exp} EXP to {tier}").replace("{exp}",remaining).replace("{tier}",tierName(nextTierIdx));nextTierHtml=`\n                <div class="profile-next-tier">\n                    <img src="${TIER_ICONS[nextTierIdx]}" alt="" class="profile-next-tier-icon">\n                    <span>${hint}</span>\n                </div>`}
1return`\n            <div class="profile-vip-card tier-${info.tierIndex}">\n                <div class="profile-vip-bg"></div>\n                <div class="profile-vip-particles" id="vipParticles"></div>\n                <div class="profile-vip-content">\n                    <div class="profile-vip-header">\n                        <div class="profile-vip-emblem">\n                            <div class="profile-vip-emblem-glow"></div>\n                            <img class="profile-vip-emblem-icon" src="${info.icon}" alt="">\n                        </div>\n                        <div class="profile-vip-title-area">\n                            <div class="profile-vip-tier-name">${name}</div>\n                            <div class="profile-vip-level-tag">Lv.${level}</div>\n                        </div>\n                        <div class="profile-vip-exp-gem">${vipData.totalExp} <span>EXP</span></div>\n                    </div>\n                    <div class="profile-tier-progress-bar">${tierDots}</div>\n                    <div class="profile-vip-exp-section">\n                        <div class="profile-vip-exp-header">\n                            <span>${t("levelProgress","Progress")}</span>\n                            <span>${progressText}</span>\n                        </div>\n                        <div class="profile-vip-exp-track">\n                            <div class="profile-vip-exp-fill tier-fill-${info.tierIndex}" style="width:${progress}%"></div>\n                        </div>\n                    </div>\n                    ${nextTierHtml}\n                    <div class="profile-vip-stats">\n                        <div class="profile-vip-stat">\n                            <div class="profile-vip-stat-icon"><i class="fas fa-fire"></i></div>\n                            <div class="profile-vip-stat-value">${vipData.consecutiveActiveDays||0}<span>${daysUnit}</span></div>\n                            <div class="profile-vip-stat-label">${t("consecutiveActive","Streak")}</div>\n                        </div>\n                        <div class="profile-vip-stat">\n                            <div class="profile-vip-stat-icon"><i class="fas fa-calendar-check"></i></div>\n                            <div class="profile-vip-stat-value">${vipData.totalActiveDays||0}<span>${daysUnit}</span></div>\n                            <div class="profile-vip-stat-label">${t("totalActive","Active")}</div>\n                        </div>\n                        <div class="profile-vip-stat">\n                            <div class="profile-vip-stat-icon"><i class="fas fa-comment-dots"></i></div>\n                            <div class="profile-vip-stat-value">${vipData.totalMessageCount||0}<span>${msgsUnit}</span></div>\n                            <div class="profile-vip-stat-label">${t("totalMessages","Messages")}</div>\n                        </div>\n                        <div class="profile-vip-stat">\n                            <div class="profile-vip-stat-icon"><i class="fa-solid fa-people-line"></i></div>\n                            <div class="profile-vip-stat-value">${vipData.socialActiveCount||0}</div>\n                            <div class="profile-vip-stat-label">${t("socialActive","Social Active")}</div>\n                        </div>\n                    </div>\n                </div>\n            </div>`}
1,renderSidebarRankNav:function renderSidebarRankNav(vipData,i18n){if(!vipData)return"";i18n&&(_i18n=i18n);const level=vipData.level||1,info=getTierInfo(level),name=vipData.levelName||`${tierName(info.tierIndex)} ${info.subName}`,nav=document.getElementById("sidebarRankNav");if(nav){for(let i=0;i<6;i++)nav.classList.remove("tier-"+i);nav.classList.add("tier-"+info.tierIndex)}return`\n            <img class="profile-rank-nav-icon" src="${info.icon}" alt="">\n            <span class="profile-rank-nav-meta">\n                <span class="profile-rank-nav-name tier-${info.tierIndex}">${name}</span>\n                <span class="profile-rank-nav-lv">Lv.${level}</span>\n            </span>`},renderPublicIdHtml:function renderPublicIdHtml(publicId){return publicId?`\n            <div class="chat-user-popup-public-id">\n                <span class="chat-user-popup-public-id-label">${window.L?.T?window.L.T("match.chat.publicId"):"ID"}</span>\n                <span class="chat-user-popup-public-id-value">${publicId}</span>\n                <button type="button" class="chat-user-popup-copy-btn" title="${window.L?.T?window.L.T("match.chat.copyId"):"Copy ID"}" data-public-id="${publicId}">\n                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>\n                </button>\n            </div>`:""},bindPublicIdCopy:function bindPublicIdCopy(popup){const btn=popup?.querySelector(".chat-user-popup-copy-btn");btn&&btn.addEventListener("click",e=>{e.stopPropagation();const id=btn.dataset.publicId;id&&navigator.clipboard&&navigator.clipboard.writeText(id).then(()=>{const msg=window.L?.T?window.L.T("match.chat.copied"):"Copied";"function"==typeof showToast&&showToast(msg,"success")})})},fetchVipInfo:async function fetchVipInfo(){try{const resp=await fetch("/Account/GetVipInfo");if(!resp.ok)return null;const result=await resp.json();return result.success?(result.i18n&&(_i18n=result.i18n),result):null}catch{return null}},fetchUserVipInfo:async function fetchUserVipInfo(username){try{const resp=await fetch(`/Account/GetUserVipInfo?username=${encodeURIComponent(username)}`);if(!resp.ok)return null;const result=await resp.json();return result.success?(result.i18n&&(_i18n=result.i18n),result):null}catch{return null}},fetchExpHistory:fetchExpHistory,initProfileExpHistory:function initProfileExpHistory(){const root=document.getElementById("profileExpHistory");if(!root)return window.__profileExpHistoryReady;if("1"===root.dataset.bound)return window.__profileExpHistoryReady;root.dataset.bound="1";const body=document.getElementById("expHistoryBody"),empty=document.getElementById("expHistoryEmpty"),pager=document.getElementById("expHistoryPager"),category=document.getElementById("expHistoryCategory"),startInput=document.getElementById("expHistoryStart"),endInput=document.getElementById("expHistoryEnd"),tableWrap=root.querySelector(".profile-exp-history-table-wrap"),state={page:1,items:[],totalPages:1,totalCount:0};let requestId=0;function renderRows(){if(!body)return;if(!state.items.length)return body.innerHTML="",tableWrap&&(tableWrap.hidden=!0),empty&&(empty.hidden=!1),void(pager&&(pager.hidden=!0,pager.innerHTML=""));if(tableWrap&&(tableWrap.hidden=!1),empty&&(empty.hidden=!0),body.innerHTML=state.items.map(item=>{const meta=function expHistoryTypeMeta(type){return"daily_checkin"===type?{label:expHistoryLabel("dailyStreak","Daily Streak"),icon:"fa-calendar-check"}:"loyalty_bonus"===type?{label:expHistoryLabel("loyaltyBonus","Loyalty Bonus"),icon:"fa-award"}:"social_bonus"===type?{label:expHistoryLabel("socialBonus","Social Bonus"),icon:"fa-people-line"}:{label:expHistoryLabel("chat","Chat"),icon:"fa-comment-dots"}}(item.type),amount=Number(item.expAmount)||0,expText=amount>0?`+${amount} EXP`:`${amount} EXP`,expClass=amount<0?"exp-history-exp is-negative":"exp-history-exp";return`<tr>\n                    <td><span class="exp-history-type exp-history-type-${item.type||"chat"}"><i class="fas ${meta.icon}" aria-hidden="true"></i>${meta.label}</span></td>\n                    <td>${function expHistoryDescription(item){return"daily_checkin"===item.type?expHistoryLabel("descCheckin","{days}-Day Active Streak Bonus ({rate} x {days})",{days:item.metaDays||1,rate:item.metaRate||10}):"loyalty_bonus"===item.type?expHistoryLabel("descLoyalty","Registered for {days} Days",{days:item.metaDays||30}):"social_bonus"===item.type?(Number(item.expAmount)||0)<0?expHistoryLabel("descUnfollow","Unfollowed a streamer"):expHistoryLabel("descFollow","Followed a streamer"):expHistoryLabel("descChat","Sent chat message in live stream")}(item)}</td>\n                    <td class="exp-history-time">${function formatExpDateTime(iso){if(!iso)return"";const date=new Date(iso);if(Number.isNaN(date.getTime()))return"";const parts=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Ho_Chi_Minh",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hour12:!1}).formatToParts(date),get=type=>(parts.find(p=>p.type===type)||{}).value||"";return`${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`}(item.createdAt)}</td>\n                    <td class="${expClass}">${expText}</td>\n                </tr>`}).join(""),!pager)return;if(state.totalCount<=20)return pager.hidden=!0,void(pager.innerHTML="");pager.hidden=!1;let html=`<button type="button" data-page="${state.page-1}"${state.page<=1?" disabled":""}>&lt;</button>`;(function pageWindow(current,total){if(total<=7)return Array.from({length:total},(_,i)=>i+1);const pages=[1],start=Math.max(2,current-1),end=Math.min(total-1,current+1);start>2&&pages.push("...");for(let i=start;
1i<=end;i++)pages.push(i);return end<total-1&&pages.push("..."),pages.push(total),pages})(state.page,state.totalPages).forEach(p=>{html+="..."===p?'<span class="profile-list-pager-ellipsis">…</span>':`<button type="button" class="${p===state.page?"is-active":""}" data-page="${p}">${p}</button>`}),html+=`<button type="button" data-page="${state.page+1}"${state.page>=state.totalPages?" disabled":""}>&gt;</button>`,pager.innerHTML=html}async function load(){if(!body)return;const id=++requestId;tableWrap&&(tableWrap.hidden=!1),function renderExpHistorySkeleton(body){body&&(body.innerHTML=Array.from({length:6},()=>'\n            <tr class="exp-history-skel-row">\n                <td><span class="skeleton profile-skeleton-line"></span></td>\n                <td><span class="skeleton profile-skeleton-line"></span></td>\n                <td><span class="skeleton profile-skeleton-line profile-skeleton-line-meta"></span></td>\n                <td><span class="skeleton profile-skeleton-line profile-skeleton-line-xs"></span></td>\n            </tr>').join(""))}(body),empty&&(empty.hidden=!0);const result=await fetchExpHistory(function currentFilters(){let start=startInput?.value||"",end=endInput?.value||"";if(start&&end&&start>end){const swap=start;start=end,end=swap}return{type:category?.value||"all",start:start,end:end,page:state.page,pageSize:20}}());if(id===requestId){if(!result||!result.data)return state.items=[],state.totalCount=0,state.totalPages=1,void renderRows();state.items=result.data.items||[],state.page=result.data.page||1,state.totalPages=result.data.totalPages||1,state.totalCount=result.data.totalCount||0,renderRows()}}return category?.addEventListener("change",()=>{state.page=1,load()}),startInput?.addEventListener("change",()=>{state.page=1,load()}),endInput?.addEventListener("change",()=>{state.page=1,load()}),pager?.addEventListener("click",e=>{const btn=e.target.closest("[data-page]");if(!btn||btn.disabled)return;const next=parseInt(btn.dataset.page,10);!next||next<1||(state.page=next,load())}),window.addEventListener("langChange",()=>{const rootSelect=category?.closest(".custom-select");if(category&&rootSelect){const label=rootSelect.querySelector(".custom-select-label"),selected=category.options[category.selectedIndex];label&&selected&&(label.textContent=(selected.textContent||"").trim()),rootSelect.querySelectorAll(".custom-select-option").forEach(btn=>{const opt=Array.from(category.options).find(o=>o.value===btn.dataset.value);opt&&(btn.textContent=(opt.textContent||"").trim())})}renderRows()}),window.__profileExpHistoryReady=load(),window.__profileExpHistoryReady}},document.addEventListener("click",e=>{const btn=e.target.closest(".chat-user-popup-copy-btn");if(!btn)return;e.preventDefault(),e.stopPropagation();const id=btn.dataset.publicId;id&&navigator.clipboard&&navigator.clipboard.writeText(id).then(()=>{const msg=window.L?.T?window.L.T("match.chat.copied"):"Copied";"function"==typeof showToast&&showToast(msg,"success")})})}(),function(){"use strict";let cropperInstance=null,modalEl=null,isUploading=!1;const T=(key,fallback)=>window.L?.T?window.L.T(key):fallback;function getI18n(){return window.avatarUploadI18n||{title:T("user.avatarUpload.title","上传头像"),
1close:T("user.avatarUpload.close","关闭"),dropzoneText:T("user.avatarUpload.dropzoneText","点击或拖拽图片到这里"),dropzoneHint:T("user.avatarUpload.dropzoneHint","支持 JPG、PNG、GIF,最大 5MB"),cropPreview:T("user.avatarUpload.cropPreview","裁剪预览"),rotateLeft:T("user.avatarUpload.rotateLeft","左旋90°"),rotateRight:T("user.avatarUpload.rotateRight","右旋90°"),zoomIn:T("user.avatarUpload.zoomIn","放大"),zoomOut:T("user.avatarUpload.zoomOut","缩小"),reselect:T("user.avatarUpload.reselect","重新选择"),confirmUpload:T("user.avatarUpload.confirmUpload","确认上传"),uploading:T("user.avatarUpload.uploading","上传中..."),fileTooLarge:T("user.avatarUpload.fileTooLarge","图片大小不能超过 5MB"),invalidFormat:T("user.avatarUpload.invalidFormat","仅支持 JPG、PNG、GIF、WebP 格式"),cropperLoadFailed:T("user.avatarUpload.cropperLoadFailed","裁剪组件加载失败,请检查网络"),uploadFailed:T("user.avatarUpload.uploadFailed","上传失败,请重试"),networkError:T("user.avatarUpload.networkError","上传失败,请检查网络")}}function handleFileSelect(e){const file=e.target.files?.[0];if(!file)return;const i18n=getI18n();if(file.size>5242880)return void alert(i18n.fileTooLarge);if(!file.type.match(/^image\/(jpeg|png|gif|webp)$/))return void alert(i18n.invalidFormat);const reader=new FileReader;reader.onload=ev=>{showStep("crop"),function initCropper(imageSrc){destroyCropper();const img=document.getElementById("avatarCropImage");img.src=imageSrc,img.onload=async()=>{await function loadCropperJs(){return new Promise(resolve=>{if(cropperLoaded)return resolve();if(!document.querySelector('link[href*="cropper"]')){const link=document.createElement("link");link.rel="stylesheet",link.href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css",document.head.appendChild(link)}const script=document.createElement("script");script.src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js",script.onload=()=>{cropperLoaded=!0,resolve()},script.onerror=()=>{alert(getI18n().cropperLoadFailed)},document.head.appendChild(script)})}(),cropperInstance=new Cropper(img,{aspectRatio:1,viewMode:1,dragMode:"move",autoCropArea:.85,cropBoxResizable:!0,cropBoxMovable:!0,background:!1,guides:!1,center:!0,highlight:!1,responsive:!0,ready(){document.getElementById("avatarBtnUpload").disabled=!1}})}}(ev.target.result)},reader.readAsDataURL(file)}let cropperLoaded="undefined"!=typeof Cropper;function destroyCropper(){cropperInstance&&(cropperInstance.destroy(),cropperInstance=null),document.getElementById("avatarCropImage").src="",document.getElementById("avatarCropInput").value="",document.getElementById("avatarBtnUpload").disabled=!0}function showStep(step){const selectEl=document.getElementById("avatarStepSelect"),cropEl=document.getElementById("avatarStepCrop"),btnBack=document.getElementById("avatarBtnBack"),btnUpload=document.getElementById("avatarBtnUpload");"select"===step?(selectEl.style.display="",cropEl.style.display="none",btnBack.style.display="none",btnUpload.style.display="none"):(selectEl.style.display="none",cropEl.style.display="",btnBack.style.display="",btnUpload.style.display="")}async function uploadCroppedAvatar(){if(!cropperInstance||isUploading)return;isUploading=!0;const btn=document.getElementById("avatarBtnUpload");btn.querySelector(".avatar-btn-text").style.display="none",btn.querySelector(".avatar-btn-loading").style.display="",btn.disabled=!0;try{const imageData=cropperInstance.getCroppedCanvas({width:256,height:256,imageSmoothingEnabled:!0,imageSmoothingQuality:"high"}).toDataURL("image/jpeg",.9),token=document.querySelector('input[name="__RequestVerificationToken"]')?.value,resp=await fetch("/profile/api/avatar",{method:"POST",headers:{"Content-Type":"application/json",RequestVerificationToken:token||""},body:JSON.stringify({imageData:imageData})}),result=await resp.json();result.success?(function updateAllAvatars(url){const fullUrl=url+("?v="+Date.now());document.querySelectorAll(".user-avatar img, .user-menu-avatar img").forEach(img=>{(img.src.includes("default-avatar")||img.src.includes("/uploads/avatars/"))&&(img.src=fullUrl)}),document.querySelectorAll(".js-profile-avatar-img, #avatarImage, .profile-overview-avatar img, .profile-card .profile-avatar img").forEach(img=>{img.src=fullUrl});const profileAvatar=document.getElementById("profileAvatar");profileAvatar&&profileAvatar.classList.remove("default-avatar")}(result.avatarUrl),close()):alert(result.message||getI18n().uploadFailed)}catch(err){console.error("Avatar upload error:",err),alert(getI18n().networkError)}finally{isUploading=!1,btn.querySelector(".avatar-btn-text").style.display="",btn.querySelector(".avatar-btn-loading").style.display="none",btn.disabled=!1}}function close(){modalEl&&(destroyCropper(),modalEl.classList.remove("active"),document.body.style.overflow="")}
1window.AvatarUpload={open:function open(){const existing=document.getElementById("globalAvatarModal");existing&&existing.remove(),modalEl=null,function createModal(){if(document.getElementById("globalAvatarModal"))return;const i18n=getI18n(),modal=document.createElement("div");modal.id="globalAvatarModal",modal.className="avatar-modal-overlay",modal.innerHTML=`\n            <div class="avatar-modal-container">\n                <div class="avatar-modal-header">\n                    <h3 class="avatar-modal-title">${i18n.title}</h3>\n                    <button class="avatar-modal-close" id="avatarModalClose" aria-label="${i18n.close}">\n                        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>\n                    </button>\n                </div>\n                <div class="avatar-modal-body">\n                    \x3c!-- 步骤1: 选择图片 --\x3e\n                    <div class="avatar-step avatar-step-select" id="avatarStepSelect">\n                        <div class="avatar-dropzone" id="avatarDropzone">\n                            <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>\n                            <p>${i18n.dropzoneText}</p>\n                            <span class="avatar-dropzone-hint">${i18n.dropzoneHint}</span>\n                        </div>\n                        <input type="file" id="avatarCropInput" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">\n                    </div>\n                    \x3c!-- 步骤2: 裁剪 --\x3e\n                    <div class="avatar-step avatar-step-crop" id="avatarStepCrop" style="display:none">\n                        <div class="avatar-crop-area">\n                            <img id="avatarCropImage" src="" alt="${i18n.cropPreview}">\n                        </div>\n                        <div class="avatar-crop-actions">\n                            <button class="avatar-crop-btn" id="avatarRotateLeft" title="${i18n.rotateLeft}">\n                                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>\n                            </button>\n                            <button class="avatar-crop-btn" id="avatarRotateRight" title="${i18n.rotateRight}">\n                                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>\n                            </button>\n                            <button class="avatar-crop-btn" id="avatarZoomIn" title="${i18n.zoomIn}">\n                                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>\n                            </button>\n                            <button class="avatar-crop-btn" id="avatarZoomOut" title="${i18n.zoomOut}">\n                                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>\n                            </button>\n                        </div>\n                    </div>\n                </div>\n                <div class="avatar-modal-footer">\n                    <button class="avatar-btn avatar-btn-cancel" id="avatarBtnBack" style="display:none">${i18n.reselect}</button>\n                    <button class="avatar-btn avatar-btn-upload" id="avatarBtnUpload" style="display:none" disabled>
1\n                        <span class="avatar-btn-text">${i18n.confirmUpload}</span>\n                        <span class="avatar-btn-loading" style="display:none">\n                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="avatar-spinner"><circle cx="12" cy="12" r="10" stroke-dasharray="31.4" stroke-dashoffset="10"></circle></svg>\n                            ${i18n.uploading}\n                        </span>\n                    </button>\n                </div>\n            </div>\n        `,document.body.appendChild(modal),modalEl=modal,function bindEvents(){const overlay=document.getElementById("globalAvatarModal"),closeBtn=document.getElementById("avatarModalClose"),dropzone=document.getElementById("avatarDropzone"),fileInput=document.getElementById("avatarCropInput"),btnBack=document.getElementById("avatarBtnBack"),btnUpload=document.getElementById("avatarBtnUpload"),rotateLeft=document.getElementById("avatarRotateLeft"),rotateRight=document.getElementById("avatarRotateRight"),zoomIn=document.getElementById("avatarZoomIn"),zoomOut=document.getElementById("avatarZoomOut");closeBtn.addEventListener("click",close),overlay.addEventListener("click",e=>{e.target===overlay&&close()}),dropzone.addEventListener("click",()=>fileInput.click()),fileInput.addEventListener("change",handleFileSelect),dropzone.addEventListener("dragover",e=>{e.preventDefault(),dropzone.classList.add("dragover")}),dropzone.addEventListener("dragleave",()=>dropzone.classList.remove("dragover")),dropzone.addEventListener("drop",e=>{e.preventDefault(),dropzone.classList.remove("dragover"),e.dataTransfer.files.length&&(fileInput.files=e.dataTransfer.files,handleFileSelect({target:fileInput}))}),rotateLeft.addEventListener("click",()=>cropperInstance?.rotate(-90)),rotateRight.addEventListener("click",()=>cropperInstance?.rotate(90)),zoomIn.addEventListener("click",()=>cropperInstance?.zoom(.1)),zoomOut.addEventListener("click",()=>cropperInstance?.zoom(-.1)),btnBack.addEventListener("click",()=>{destroyCropper(),showStep("select")}),btnUpload.addEventListener("click",uploadCroppedAvatar)}()}(),destroyCropper(),showStep("select"),modalEl.classList.add("active"),document.body.style.overflow="hidden"},close:close}}();const PUSH_DEBUG=!1;class PushNotificationManager{constructor(){this.vapidPublicKey=null,this.isSupported=!1,this.isSubscribed=!1,this.subscription=null,this.init()}log(...args){}async init(){if(this.log("[Push] 初始化推送通知管理器..."),this.isSupported=this.checkSupport(),this.isSupported){this.log("[Push] 获取VAPID公钥..."),await this.loadVapidKey(),this.log("[Push] 检查当前订阅状态...");try{await Promise.race([this.checkSubscriptionStatus(),new Promise((_,reject)=>setTimeout(()=>reject(new Error("Service Worker 初始化超时")),5e3))])}catch(error){console.error("[Push] ✗ 初始化超时或失败:",error),this.log("[Push] 尝试手动注册 Service Worker...");try{await navigator.serviceWorker.register("/sw.js"),this.log("[Push] Service Worker 手动注册成功,重新检查订阅状态..."),await this.checkSubscriptionStatus()}catch(swError){return void console.error("[Push] ✗ Service Worker 注册失败:",swError)}}this.isSubscribed&&this.subscription&&this.verifySubscriptionValidity(),this.updateUI(),this.log("[Push] 推送通知管理器初始化完成"),this.log("[Push] - 支持状态:",this.isSupported?"✓ 支持":"✗ 不支持"),this.log("[Push] - 订阅状态:",this.isSubscribed?"✓ 已订阅":"✗ 未订阅")}else this.log("[Push] ✗ 浏览器不支持推送通知")}checkSupport(){if(!("serviceWorker"in navigator))return this.log("[Push] Service Worker not supported"),!1;if(!("PushManager"in window))return this.log("[Push] Push messaging not supported"),!1;if(!("Notification"in window))return this.log("[Push] Notifications not supported"),!1;if(/iPad|iPhone|iPod/.test(navigator.userAgent)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1){this.log("[Push] 检测到 iOS 设备");const iosVersion=this.getIOSVersion();if(iosVersion&&iosVersion.major<16)return iosVersion.major,iosVersion.minor,!1;this.log("[Push] iOS 设备基本支持检测通过")}return!0}isIOSInPWAMode(){return!(/iPad|iPhone|iPod/.test(navigator.userAgent)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1)||window.matchMedia("(display-mode: standalone)").matches||window.navigator.standalone}getIOSVersion(){const match=navigator.userAgent.match(/OS (\d+)_(\d+)/);return match?{major:parseInt(match[1]),minor:parseInt(match[2])}:null}async loadVapidKey(){try{const response=await fetch("/api/push/vapid-key");if(response.ok){const data=await response.json();if(this.vapidPublicKey=data.publicKey,this.log("[Push] VAPID key loaded successfully"),this.vapidPublicKey){this.log("[Push] VAPID 公钥长度:",this.vapidPublicKey.length),this.log("[Push] VAPID 公钥格式验证...");try{const uint8Array=await this.urlBase64ToUint8Array(this.vapidPublicKey);this.log("[Push] VAPID 公钥转换为 Uint8Array 成功,长度:",uint8Array.length),65!==uint8Array.length?console.error("[Push] ✗ VAPID 公钥长度不正确,期望 65 字节,实际:",uint8Array.length):this.log("[Push] ✓ VAPID 公钥格式正确")}catch(error){console.error("[Push] ✗ VAPID 公钥格式验证失败:",error)}}}else console.error("[Push] Failed to load VAPID key from server"),this.vapidPublicKey="BEl62iUYgUivxIkv69yViEuiBIa40HI6YUldNzqjJuQlhrA_CCgO9Kh8FUWZPsOgB4shWY8qnNHYwxGBRNaEDlY"}catch(error){console.error("[Push] Failed to load VAPID key:",error),this.vapidPublicKey="BEl62iUYgUivxIkv69yViEuiBIa40HI6YUldNzqjJuQlhrA_CCgO9Kh8FUWZPsOgB4shWY8qnNHYwxGBRNaEDlY"}}async checkSubscriptionStatus(){try{this.log("[Push] 等待 Service Worker 准备就绪...");const registration=await navigator.serviceWorker.ready;this.log("[Push] Service Worker 已准备就绪"),this.log("[Push] 获取推送订阅...");const subscription=await registration.pushManager.getSubscription();this.log("[Push] 获取订阅完成"),this.subscription=subscription,this.isSubscribed=!!subscription,this.isSubscribed&&subscription?(this.log("[Push] ✓ 发现已存在的订阅"),this.log("[Push] - Endpoint:",subscription.endpoint),this.log("[Push] - P256dh:",subscription.getKey("p256dh")?"已设置":"未设置"),this.log("[Push] - Auth:",subscription.getKey("auth")?"已设置":"未设置")):this.log("[Push] 未找到活跃的订阅"),this.updateUI()}catch(error){console.error("[Push] ✗ 检查订阅状态失败:",error),console.error("[Push] ✗ 错误堆栈:",error.stack)}}
1async verifySubscriptionValidity(){try{this.log("[Push] 开始验证订阅有效性...");const subscriptionData={endpoint:this.subscription.endpoint,keys:{p256dh:btoa(String.fromCharCode.apply(null,new Uint8Array(this.subscription.getKey("p256dh")))),auth:btoa(String.fromCharCode.apply(null,new Uint8Array(this.subscription.getKey("auth"))))}},response=await fetch("/api/push/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({subscription:subscriptionData})});if(!response.ok)return void console.warn("[Push] 验证API返回错误:",response.status);const contentType=response.headers.get("content-type");if(!contentType||!contentType.includes("application/json"))return void console.warn("[Push] 验证API返回非JSON响应,可能端点不存在");const result=await response.json();result.isValid?this.log("[Push] ✓ 订阅有效"):(console.warn("[Push] ⚠️ 订阅已失效:",result.reason),this.isSubscribed=!1,this.updateUI(),this.showResubscribePrompt(result.reason))}catch(error){this.log("[Push] 验证订阅有效性失败(已忽略):",error.message)}}showResubscribePrompt(reason){if("true"===localStorage.getItem("push_dismiss_resubscribe"))return void this.log("[Push] 用户已选择不再提示重新订阅");const modal=document.createElement("div");modal.className="push-resubscribe-modal",modal.innerHTML=`\n            <div class="resubscribe-overlay"></div>\n            <div class="resubscribe-content">\n                <div class="resubscribe-icon">\n                    <i class="fas fa-bell-slash"></i>\n                </div>\n                <h3>推送通知已失效</h3>\n                <p class="reason-text">${reason||"您的推送订阅已过期,需要重新开启以继续接收比赛通知"}</p>\n                <div class="resubscribe-actions">\n                    <button class="btn-resubscribe" onclick="window.pushManager.resubscribeNow()">\n                        <i class="fas fa-bell"></i>\n                        重新开启通知\n                    </button>\n                    <button class="btn-dismiss" onclick="localStorage.setItem('push_dismiss_resubscribe', 'true'); this.closest('.push-resubscribe-modal').remove();">\n                        不再提示\n                    </button>\n                </div>\n            </div>\n        `;const style=document.createElement("style");style.textContent="\n            .push-resubscribe-modal {\n                position: fixed;\n                top: 0;\n                left: 0;\n                width: 100%;\n                height: 100%;\n                z-index: 10002;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                animation: fadeIn 0.3s ease;\n            }\n            \n            .resubscribe-overlay {\n                position: absolute;\n                top: 0;\n                left: 0;\n                width: 100%;\n                height: 100%;\n                background: rgba(0, 0, 0, 0.6);\n                backdrop-filter: blur(4px);\n            }\n            \n            .resubscribe-content {\n                position: relative;\n                background: linear-gradient(135deg, #10b981 0%, #059669 100%);\n                border-radius: 20px;\n                padding: 32px;\n                max-width: 400px;\n                width: 90%;\n                box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n                text-align: center;\n                color: white;\n                animation: slideUp 0.3s ease;\n            }\n            \n            .resubscribe-icon {\n                width: 80px;\n                height: 80px;\n                margin: 0 auto 20px;\n                background: rgba(255, 255, 255, 0.2);\n                border-radius: 50%;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                font-size: 36px;\n            }\n            \n            .resubscribe-content h3 {\n                font-size: 24px;\n                font-weight: 700;\n                margin: 0 0 16px 0;\n                color: white;\n            }\n            \n            .reason-text {\n                font-size: 15px;\n                line-height: 1.6;\n                margin: 0 0 24px 0;\n                color: rgba(255, 255, 255, 0.95);\n            }\n            \n            .resubscribe-actions {\n                display: flex;\n                flex-direction: column;\n                gap: 12px;\n            }\n            \n            .btn-resubscribe {\n                background: white;\n                color: #059669;\n                border: none;\n                padding: 14px 28px;\n                border-radius: 12px;\n                font-size: 16px;\n                font-weight: 600;\n                cursor: pointer;\n                transition: all 0.3s ease;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                gap: 8px;\n            }\n            \n            .btn-resubscribe:hover {\n                transform: translateY(-2px);\n                box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);\n            }\n            \n            .btn-dismiss {\n                background: transparent;\n                color: white;\n                border: 2px solid rgba(255, 255, 255, 0.3);\n                padding: 12px 28px;\n                border-radius: 12px;\n                font-size: 14px;\n                font-weight: 500;\n                cursor: pointer;\n                transition: all 0.3s ease;\n            }\n            \n            .btn-dismiss:hover {\n                background: rgba(255, 255, 255, 0.1);\n                border-color: rgba(255, 255, 255, 0.5);\n            }\n            \n            @keyframes fadeIn {\n                from { opacity: 0; }\n                to { opacity: 1; }\n            }\n            \n            @keyframes slideUp {\n                from {\n                    opacity: 0;\n                    transform: translateY(30px);\n                }\n                to {\n                    opacity: 1;\n                    transform: translateY(0);\n                }\n            }\n        ",document.head.appendChild(style),document.body.appendChild(modal)}async resubscribeNow(){try{const modal=document.querySelector(".push-resubscribe-modal");modal&&modal.remove(),this.subscription&&await this.subscription.unsubscribe(),await this.subscribe(),this.showNotificationToast("✓ 推送通知已重新开启","success")}catch(error){console.error("[Push] 重新订阅失败:",error),this.showNotificationToast("重新订阅失败,请稍后再试","error")}}async subscribe(){if(!this.isSupported)throw new Error("Push notifications not supported");if(!this.vapidPublicKey)throw new Error("VAPID key not loaded");if("granted"!==Notification.permission&&"granted"!==await Notification.requestPermission())throw new Error("Notification permission denied");return await this.subscribeInternal()}async subscribeInternal(){if(!this.isSupported)throw new Error("Push notifications not supported");if(!this.vapidPublicKey)throw new Error("VAPID key not loaded");if("granted"!==Notification.permission)throw new Error("Notification permission denied");if(!this.isIOSInPWAMode())throw alert('📱 iOS设备需要先添加到主屏幕才能开启推送通知\n\n请按以下步骤操作:\n1. 点击底部的分享按钮 📤\n2. 选择"添加到主屏幕"\n3. 从主屏幕打开应用\n4. 再次点击订阅按钮'),new Error("iOS device requires PWA mode for push notifications");try{this.log("[Push] 开始订阅流程..."),this.log("[Push] VAPID 公钥:",this.vapidPublicKey);let subscription,applicationServerKey,registration=await navigator.serviceWorker.getRegistration();if(registration?this.log("[Push] Service Worker 已注册"):(this.log("[Push] Service Worker 未注册,开始注册..."),registration=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),this.log("[Push] Service Worker 注册成功")),await navigator.serviceWorker.ready,this.log("[Push] Service Worker 已就绪"),/iPad|iPhone|iPod/.test(navigator.userAgent)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1){this.log("[Push] iOS设备检测到,使用优化的订阅方法...");try{applicationServerKey=await this.urlBase64ToUint8Array(this.vapidPublicKey),this.log("[Push] ✓ iOS VAPID公钥转换成功")}catch(e){this.log("[Push] VAPID转换失败,尝试硬编码公钥:",e),applicationServerKey=new Uint8Array([4,40,162,133,197,244,249,91,207,104,78,199,154,144,82,172,20,94,151,127,17,189,20,114,109,160,251,136,231,144,213,2,209,143,146,65,152,50,26,130,255,89,46,19,179,250,51,0,132,73,64,208,157,204,64,50,218,163,247,102,16,206,175,232])}try{this.log("[Push] 尝试iOS订阅..."),subscription=await registration.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:applicationServerKey}),this.log("[Push] ✓ iOS订阅成功")}catch(error){this.log("[Push] iOS订阅失败,尝试不带公钥:",error.message);try{subscription=await registration.pushManager.subscribe({userVisibleOnly:!0}),this.log("[Push] ✓ iOS无公钥订阅成功")}catch(error2){throw console.error("[Push] ✗ iOS所有订阅方法都失败:",error2),error2}}}else this.log("[Push] 非iOS设备,使用标准订阅方法..."),applicationServerKey=await this.urlBase64ToUint8Array(this.vapidPublicKey),subscription=await registration.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:applicationServerKey});if(this.log("[Push] 推送订阅创建成功"),this.log("[Push] 订阅端点:",subscription.endpoint),this.log("[Push] 发送订阅信息到服务器..."),await this.sendSubscriptionToServer(subscription))return this.subscription=subscription,this.isSubscribed=!0,this.updateUI(),this.log("[Push] ✓✓✓ 订阅流程完成!推送通知已成功开启 ✓✓✓"),!0;throw new Error("Failed to save subscription on server")}catch(error){throw console.error("[Push] ✗✗✗ 订阅失败:",error),console.error("[Push] ✗✗✗ 错误名称:",error.name),console.error("[Push] ✗✗✗ 错误详情:",error.message),console.error("[Push] ✗✗✗ 错误堆栈:",error.stack),/iPad|iPhone|iPod/.test(navigator.userAgent)&&error.message.includes("applicationServerKey")&&alert("iOS设备订阅失败:VAPID公钥格式错误\n\n请尝试:\n1. 确保已添加到主屏幕\n2. 清除Safari数据后重试\n3. 更新iOS到最新版本"),error}}async unsubscribe(){try{if(this.subscription)return await this.subscription.unsubscribe(),this.subscription=null,this.isSubscribed=!1,this.updateUI(),this.log("[Push] Successfully unsubscribed from push notifications"),!0}catch(error){throw console.error("[Push] Failed to unsubscribe:",error),error}}async sendSubscriptionToServer(subscription){try{const response=await fetch("/api/push/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({subscription:{endpoint:subscription.endpoint,keys:{p256dh:arrayBufferToBase64(subscription.getKey("p256dh")),auth:arrayBufferToBase64(subscription.getKey("auth"))}}
1,userAgent:navigator.userAgent,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone})});if(response.ok){const result=await response.json();return this.log("[Push] ✓ 订阅成功!服务器响应:",result),this.log("[Push] ✓ 订阅状态: 已保存到数据库"),this.log("[Push] ✓ Endpoint:",subscription.endpoint),!0}{const error=await response.json();return console.error("[Push] ✗ 订阅失败!服务器错误:",error.error),console.error("[Push] ✗ HTTP状态:",response.status),!1}}catch(error){return console.error("[Push] ✗ 网络错误!无法连接到服务器:",error),!1}}async requestPermissionDirectly(){try{if("granted"===Notification.permission)return void(this.isSubscribed||(await this.subscribeInternal(),this.showNotificationToast("✓ 推送通知已开启!","success")));if("denied"===Notification.permission)return;const permission=await Notification.requestPermission();this.log("[Push] 通知权限请求结果:",permission),"granted"===permission&&(await this.subscribeInternal(),this.showNotificationToast("✓ 推送通知已开启!","success"))}catch(error){console.error("[Push] 权限请求失败:",error)}}shouldAutoSubscribe(){if("true"===localStorage.getItem("push-auto-subscribe-declined"))return!1;if("denied"===Notification.permission)return!1;if("granted"===Notification.permission)return!0;const isIOS=/iPad|iPhone|iPod/.test(navigator.userAgent)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1,isPWA=window.matchMedia("(display-mode: standalone)").matches||window.navigator.standalone;return isIOS&&isPWA}showAutoSubscriptionSuccess(){this.showNotificationToast("✓ 推送通知已自动开启","success")}showNotificationToast(message,type="info"){const existingToast=document.querySelector(".push-notification-toast");existingToast&&existingToast.remove();const toast=document.createElement("div");toast.className=`push-notification-toast ${type}`,toast.innerHTML=`\n            <div class="toast-content">\n                <i class="fas fa-bell"></i>\n                <span>${message}</span>\n            </div>\n            <button class="toast-close" onclick="this.parentElement.remove()">\n                <i class="fas fa-times"></i>\n            </button>\n        `;const style=document.createElement("style");style.textContent="\n            .push-notification-toast {\n                position: fixed;\n                top: 80px;\n                right: 20px;\n                background: rgba(34, 197, 94, 0.95);\n                color: white;\n                padding: 16px 20px;\n                border-radius: 12px;\n                box-shadow: 0 4px 20px rgba(34, 197, 94, 0.3);\n                backdrop-filter: blur(10px);\n                border: 1px solid rgba(34, 197, 94, 0.3);\n                display: flex;\n                align-items: center;\n                gap: 12px;\n                z-index: 10000;\n                max-width: 350px;\n                animation: slideInRight 0.3s ease;\n                font-size: 14px;\n            }\n            \n            .push-notification-toast.error {\n                background: rgba(239, 68, 68, 0.95);\n                box-shadow: 0 4px 20px rgba(239, 68, 68, 0.3);\n                border-color: rgba(239, 68, 68, 0.3);\n            }\n            \n            .push-notification-toast .toast-content {\n                display: flex;\n                align-items: center;\n                gap: 8px;\n                flex: 1;\n            }\n            \n            .push-notification-toast .toast-close {\n                background: none;\n                border: none;\n                color: white;\n                cursor: pointer;\n                font-size: 16px;\n                padding: 4px;\n                border-radius: 4px;\n                opacity: 0.8;\n                transition: all 0.2s ease;\n            }\n            \n            .push-notification-toast .toast-close:hover {\n                opacity: 1;\n                background: rgba(255, 255, 255, 0.1);\n            }\n            \n            @keyframes slideInRight {\n                from {\n                    transform: translateX(100%);\n                    opacity: 0;\n                }\n                to {\n                    transform: translateX(0);\n                    opacity: 1;\n                }\n            }\n            \n            @media (max-width: 768px) {\n                .push-notification-toast {\n                    right: 16px;\n                    left: 16px;\n                    max-width: none;\n                }\n            }\n        ",document.head.appendChild(style),document.body.appendChild(toast),setTimeout(()=>{toast.parentElement&&(toast.style.animation="slideInRight 0.3s ease reverse",setTimeout(()=>toast.remove(),300))},3e3)}showSubscriptionPrompt(){if("true"===localStorage.getItem("push-prompt-shown"))return;const isIOS=/iPad|iPhone|iPod/.test(navigator.userAgent)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;setTimeout(()=>
1{this.showGreenSubscriptionModal(isIOS)},3e3)}showGreenSubscriptionModal(isIOS=!1){const existingModal=document.querySelector(".push-subscription-modal");existingModal&&existingModal.remove();const modal=document.createElement("div");modal.className="push-subscription-modal",modal.innerHTML=`\n            <div class="subscription-modal-overlay" onclick="window.pushManager.dismissSubscriptionModal()">\n            </div>\n            <div class="subscription-modal-content">\n                <div class="modal-header">\n                    <div class="modal-icon">\n                        <i class="fas fa-bell"></i>\n                    </div>\n                    <h3>开启推送通知</h3>\n                    <button class="modal-close" onclick="window.pushManager.dismissSubscriptionModal()">\n                        <i class="fas fa-times"></i>\n                    </button>\n                </div>\n                <div class="modal-body">\n                    <p>获取比赛开始提醒</p>\n                    ${isIOS?'\n                        <div class="ios-tips">\n                            <i class="fas fa-info-circle"></i>\n                            <span>iOS需添加到主屏幕</span>\n                        </div>\n                    ':""}\n                </div>\n                <div class="modal-actions">\n                    <button class="btn-decline" onclick="window.pushManager.declineSubscription()">\n                        暂不开启\n                    </button>\n                    <button class="btn-subscribe" onclick="window.pushManager.acceptSubscription()">\n                        <i class="fas fa-bell"></i>\n                        立即开启\n                    </button>\n                </div>\n            </div>\n        `;const style=document.createElement("style");style.textContent="\n            .push-subscription-modal {\n                position: fixed;\n                top: 0;\n                left: 0;\n                width: 100%;\n                height: 100%;\n                z-index: 10001;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                animation: fadeIn 0.3s ease;\n            }\n            \n            .subscription-modal-overlay {\n                position: absolute;\n                top: 0;\n                left: 0;\n                width: 100%;\n                height: 100%;\n                background: rgba(0, 0, 0, 0.7);\n                backdrop-filter: blur(4px);\n            }\n            \n            .subscription-modal-content {\n                position: relative;\n                background: #1f2937;\n                border: 1px solid #374151;\n                border-radius: 12px;\n                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n                max-width: 360px;\n                width: 90%;\n                animation: slideUp 0.3s ease;\n                overflow: hidden;\n            }
1\n            \n            .modal-header {\n                padding: 20px 20px 12px;\n                text-align: center;\n                position: relative;\n                background: linear-gradient(180deg, #22c55e 0%, #16a34a 100%);\n            }\n            \n            .modal-icon {\n                width: 48px;\n                height: 48px;\n                background: rgba(255, 255, 255, 0.2);\n                border-radius: 50%;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                margin: 0 auto 12px;\n                backdrop-filter: blur(10px);\n            }\n            \n            .modal-icon i {\n                color: white;\n                font-size: 20px;\n            }\n            \n            .modal-header h3 {\n                margin: 0;\n                color: white;\n                font-size: 18px;\n                font-weight: 600;\n            }\n            \n            .modal-close {\n                position: absolute;\n                top: 12px;\n                right: 12px;\n                background: rgba(255, 255, 255, 0.1);\n                border: none;\n                font-size: 16px;\n                color: white;\n                cursor: pointer;\n                padding: 6px;\n                border-radius: 50%;\n                transition: all 0.2s ease;\n                width: 28px;\n                height: 28px;\n                display: flex;\n                align-items: center;\n                justify-content: center;\n            }
1\n            \n            .modal-close:hover {\n                background: rgba(255, 255, 255, 0.2);\n            }\n            \n            .modal-body {\n                padding: 16px 20px;\n                text-align: center;\n                background: #1f2937;\n            }\n            \n            .modal-body p {\n                margin: 0 0 12px;\n                color: #e5e7eb;\n                font-size: 15px;\n                line-height: 1.4;\n            }\n            \n            .ios-tips {\n                display: flex;\n                align-items: center;\n                justify-content: center;\n                gap: 6px;\n                background: rgba(34, 197, 94, 0.1);\n                border: 1px solid rgba(34, 197, 94, 0.2);\n                border-radius: 6px;\n                padding: 8px 12px;\n                color: #86efac;\n                font-size: 13px;\n            }\n            \n            .ios-tips i {\n                font-size: 12px;\n            }\n            \n            .modal
1-actions {\n                padding: 12px 20px 20px;\n                display: flex;\n                gap: 10px;\n                background: #1f2937;\n            }\n            \n            .btn-decline {\n                background: transparent;\n                border: 1px solid #4b5563;\n                color: #9ca3af;\n                padding: 10px 20px;\n                border-radius: 6px;\n                font-size: 14px;\n                cursor: pointer;\n                transition: all 0.2s ease;\n                flex: 1;\n            }\n            \n            .btn-decline:hover {\n                background: rgba(107, 114, 128, 0.1);\n                border-color: #6b7280;\n                color: #d1d5db;\n            }\n            \n            .btn-subscribe {\n                background: linear-gradient(135deg, #22c55e, #16a34a);\n                border: none;\n                color: white;\n                padding: 10px 20px;\n                border-radius: 6px;\n                font-size: 14px;\n                font-weight: 600;\n                cursor: pointer;\n                display: flex;\n                align-items: center;\n                gap: 6px;\n                box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);\n                transition: all 0.2s ease;\n                flex: 1;\n                justify-content: center;\n            }\n            \n            .btn-subscribe:hover {\n                transform: translateY(-1px);\n                box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);\n            }\n            \n            .btn-subscribe:active {\n                transform: translateY(0);\n            }\n            \n            @keyframes fadeIn {\n                from { opacity: 0; }\n                to { opacity: 1; }\n            }\n            \n            @keyframes slideUp {\n                from {\n                    transform: translateY(20px);\n                    opacity: 0;\n                }\n                to {\n                    transform: translateY(0);\n                    opacity: 1;\n                }\n            }\n            \n            @media (max-width: 768px) {\n                .subscription-modal-content {\n                    margin: 20px;\n                    width: auto;\n                }
1\n                \n                .modal-actions {\n                    flex-direction: column;\n                }\n                \n                .btn-decline,\n                .btn-subscribe {\n                    width: 100%;\n                }\n            }\n        ",document.head.appendChild(style),window.pushManager||(window.pushManager=this);const manager=window.pushManager;manager.acceptSubscription=async()=>{try{await manager.subscribe(),manager.dismissSubscriptionModal(),manager.showNotificationToast("✓ 推送通知已开启!","success")}catch(error){console.error("Subscription failed:",error),isIOS?error.message.includes("permission")?manager.showNotificationToast("请在设置中允许通知权限","error"):window.matchMedia("(display-mode: standalone)").matches?manager.showNotificationToast("订阅失败,请重试","error"):manager.showNotificationToast("请先将网站添加到主屏幕","error"):manager.showNotificationToast("订阅失败,请重试","error")}},manager.declineSubscription=()=>{localStorage.setItem("push-auto-subscribe-declined","true"),manager.dismissSubscriptionModal()},manager.dismissSubscriptionModal=()=>{localStorage.setItem("push-prompt-shown","true"),modal.style.animation="fadeIn 0.3s ease reverse",setTimeout(()=>modal.remove(),300)},document.body.appendChild(modal)}updateUI(){[document.getElementById("push-subscribe-btn"),document.getElementById("mobile-push-subscribe-btn")].forEach(button=>{if(button)if(this.isSupported){button.style.display="inline-flex","push-subscribe-btn"===button.id&&button.classList.contains("global-fab-btn")&&button.classList.add("visible");const icon=button.querySelector("i");this.isSubscribed?(button.classList.add("push-btn-active"),icon&&(icon.className="fas fa-volume-up"),button.style.pointerEvents="none",button.style.cursor="default",button.title="已开启推送通知"):(button.classList.remove("push-btn-active"),icon&&(icon.className="fas fa-volume-mute"),button.style.pointerEvents="auto",button.style.cursor="pointer",button.title=window.L?.T?window.L.T("push.enableNotifications"):"开启推送通知")}else button.style.display="none","push-subscribe-btn"===button.id&&button.classList.contains("global-fab-btn")&&button.classList.remove("visible")}),"function"==typeof window.updateFabContainerVisibility&&window.updateFabContainerVisibility()}updateButtonState(button){if(!this.isSupported)return void(button.style.display="none");button.style.display="inline-flex",button.disabled=!1;const icon=button.querySelector("i");this.isSubscribed?(icon&&(icon.className="fas fa-bell",icon.style.color="#16a34a"),button.title="通知已开启"):(icon&&(icon.className="fas fa-bell-slash",icon.style.color=""),button.title="开启通知")}async urlBase64ToUint8Array(base64String){try{if(!base64String||"string"!=typeof base64String)throw console.error("[Push] ✗ 无效的 base64 字符串"),new Error("Invalid base64 string: input is null or not a string");const cleanBase64=base64String.trim();try{const base64=(cleanBase64+"=".repeat((4-cleanBase64.length%4)%4)).replace(/\-/g,"+").replace(/_/g,"/"),binaryString=atob(base64),bytes=new Uint8Array(binaryString.length);for(let i=0;i<binaryString.length;i++)bytes[i]=binaryString.charCodeAt(i);if(65===bytes.length&&4===bytes[0])return bytes;this.log("[Push] 转换结果不是有效的P-256公钥格式,长度:",bytes.length,"首字节:",bytes[0]?.toString(16))}catch(e){this.log("[Push] 标准转换失败:",e.message)}try{this.log("[Push] 尝试Fetch API解码方案...");const base64Url=cleanBase64.replace(/-/g,"+").replace(/_/g,"/"),response=await fetch(`data:application/octet-stream;base64,${base64Url}`),arrayBuffer=await response.arrayBuffer(),bytes=new Uint8Array(arrayBuffer);if(65===bytes.length&&4===bytes[0])return this.log("[Push] ✓ Fetch API解码成功,长度:",bytes.length),bytes}catch(e){this.log("[Push] Fetch API解码失败:",e.message)}try{return new Uint8Array([4,10,86,235,7,68,69,23,58,167,7,44,82,203,199,18,111,56,241,73,28,135,227,151,193,155,193,103,90,161,18,100,60,67,255,155,232,247,114,146,0,157,38,82,150,72,110,242,135,228,9,183,167,186,180,0,183,202,182,47,105,47,133,47,151])}catch(fallbackError){console.error("[Push] ✗ 硬编码公钥也失败:",fallbackError)}throw new Error("Failed to convert VAPID public key for iOS")}catch(error){throw console.error("[Push] ✗ iOS VAPID 转换失败:",error),error}}}function arrayBufferToBase64(buffer){const bytes=new Uint8Array(buffer);let binary="";
1return bytes.forEach(byte=>binary+=String.fromCharCode(byte)),window.btoa(binary)}let pushManager=null;function showNotification(message,type="info"){type.toUpperCase(),"function"==typeof window.showToast?window.showToast(message,type):alert(message)}document.addEventListener("DOMContentLoaded",async()=>{pushManager=new PushNotificationManager,window.pushManager=pushManager;const desktopBtn=document.getElementById("push-subscribe-btn"),mobileBtn=document.getElementById("mobile-push-subscribe-btn");async function handlePushButtonClick(button){if(!pushManager.isSubscribed)try{button.disabled=!0;const icon=button.querySelector("i");icon&&(icon.className="fas fa-spinner fa-spin"),await pushManager.subscribe(),showNotification(window.L?.T?window.L.T("push.subscriptionSuccess"):"推送通知已成功开启!","success")}catch(error){console.error("[Push] ✗ 操作失败:",error),console.error("[Push] ✗ 详细错误:",error.message),showNotification((window.L?.T?window.L.T("push.operationFailed"):"操作失败")+":"+error.message,"error")}finally{button.disabled=!1,pushManager.updateUI()}}desktopBtn&&desktopBtn.addEventListener("click",()=>handlePushButtonClick(desktopBtn)),mobileBtn&&mobileBtn.addEventListener("click",()=>handlePushButtonClick(mobileBtn))}),"serviceWorker"in navigator&&navigator.serviceWorker.addEventListener("message",event=>{if(event.data&&"NAVIGATE_TO"===event.data.type){const url=event.data.url;url&&url!==window.location.pathname&&(window.location.href=url)}}),window.PushNotificationManager=PushNotificationManager,window.pushManager=pushManager,window.debugPushSubscription=async function(){try{const isIOS=/iPad|iPhone|iPod/.test(navigator.userAgent)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;if(/Safari/.test(navigator.userAgent)&&/Chrome|CriOS|FxiOS/.test(navigator.userAgent),isIOS){const iosVersion=pushManager.getIOSVersion();if(iosVersion&&iosVersion.major<16)return void console.error("[Push Debug] ✗ iOS 版本过低,需要 iOS 16.4+ 才支持 Web Push");if(!window.matchMedia("(display-mode: standalone)").matches&&!window.navigator.standalone)return void console.error("[Push Debug] ✗ iOS 设备未在 PWA 模式下运行")}const registration=await navigator.serviceWorker.ready;if(pushManager?.vapidPublicKey)try{await pushManager.urlBase64ToUint8Array(pushManager.vapidPublicKey)}catch(error){return void console.error("[Push Debug] ✗ VAPID 公钥格式错误:",error)}const permission=Notification.permission;if("denied"===permission)return void console.error("[Push Debug] ✗ 通知权限被拒绝");await registration.pushManager.getSubscription()||"granted"===permission&&pushManager&&!pushManager.isSubscribed&&await pushManager.subscribe()}catch(error){console.error("[Push Debug] 调试过程中出错:",error),console.error("[Push Debug] 错误堆栈:",error.stack)}};class ServiceWorkerAutoUpdater{constructor(){this.currentVersion=null,this.isUpdating=!1,this.pendingControllerReload=!1,this.checkInterval=6e4,this.maxRetries=3,this.retryCount=0,this.silentMode=!window.location.hostname.includes("localhost")&&!window.location.hostname.includes("127.0.0.1"),this.ivsInitializing=!1,this.init()}
1async init(){if("serviceWorker"in navigator)try{const registration=await navigator.serviceWorker.register("/sw.js",{scope:"/",updateViaCache:"none"});await this.checkVersion(),this.setupEventListeners(registration),this.startPeriodicCheck(),document.addEventListener("visibilitychange",()=>{document.hidden||this.checkVersion()})}catch(error){console.error("[SW-AU] Service Worker registration failed:",error),this.retryRegistration()}}async checkVersion(){if(!this.isUpdating)try{const registration=await navigator.serviceWorker.getRegistration();if(!registration)return;if(registration.waiting)return void await this.activateNewVersion(registration.waiting);await registration.update();const currentSW=registration.active||registration.installing;if(currentSW){const version=await this.getSWVersion(currentSW);this.currentVersion&&this.currentVersion!==version?(this.currentVersion,this.currentVersion=version,await this.handleVersionUpdate()):this.currentVersion||(this.currentVersion=version)}this.retryCount=0}catch(error){console.error("[SW-AU] Version check failed:",error),this.handleCheckError()}}async getSWVersion(serviceWorker){return new Promise(resolve=>{const messageChannel=new MessageChannel;messageChannel.port1.onmessage=event=>{resolve(event.data.version||"unknown")},serviceWorker.postMessage({type:"GET_VERSION"},[messageChannel.port2]),setTimeout(()=>resolve("timeout"),3e3)})}setupEventListeners(registration){registration.addEventListener("updatefound",()=>{const newWorker=registration.installing;newWorker&&newWorker.addEventListener("statechange",()=>{newWorker.state,"installed"===newWorker.state&&navigator.serviceWorker.controller&&this.activateNewVersion(newWorker)})}),navigator.serviceWorker.addEventListener("message",event=>{if("SW_UPDATED"===event.data.type){event.data;const lastReload=localStorage.getItem("sw_last_reload"),now=Date.now();if(lastReload&&now-parseInt(lastReload)<5e3)return;"clear_and_reload"===event.data.action?this.clearAllCachesAndReload(event.data.version):"silent_reload"===event.data.action&&this.performSilentReload()}}),navigator.serviceWorker.addEventListener("controllerchange",()=>{if(window._ivsInitializing||this.ivsInitializing)return;const lastReload=localStorage.getItem("sw_last_reload"),now=Date.now();lastReload&&now-parseInt(lastReload)<5e3||(localStorage.setItem("sw_last_reload",now.toString()),this.pendingControllerReload=!0,this.clearAllCachesAndReload("controller-change"))})}async activateNewVersion(newWorker){if(!this.isUpdating){this.isUpdating=!0,this.pendingControllerReload=!0;try{newWorker.postMessage({type:"SKIP_WAITING"}),await new Promise(resolve=>setTimeout(resolve,500))}catch(error){console.error("[SW-AU] Failed to activate new version:",error),this.isUpdating=!1}}}async handleVersionUpdate(){this.silentMode||this.showUpdateIndicator(),setTimeout(()=>{this.performSilentReload()},this.silentMode?300:1e3)}async clearAllCachesAndReload(newVersion){const lastReload=localStorage.getItem("sw_last_reload"),now=Date.now();if(lastReload&&now-parseInt(lastReload)<5e3)this.isUpdating=!1;else if(!this.isUpdating){this.isUpdating=!0,localStorage.setItem("sw_last_reload",now.toString());try{if("caches"in window){const cacheNames=await caches.keys();await Promise.all(cacheNames.map(name=>caches.delete(name)))}const keysToKeep=["user_language","theme","cookie_consent","sw_last_reload"];Object.keys(localStorage).forEach(key=>{keysToKeep.some(k=>key.includes(k))||localStorage.removeItem(key)}),keysToKeep.join(", "),sessionStorage.clear(),localStorage.setItem("sw_version",newVersion),localStorage.setItem("sw_updated_at",Date.now().toString()),setTimeout(()=>{window.location.reload(!0)},100)}catch(error){console.error("[SW-AU] Failed to clear caches:",error),window.location.reload(!0)}}}performSilentReload(force=!1){this.isUpdating&&!force||(this.isUpdating=!0,this.silentMode||this.showReloadIndicator(),this.savePageState(),setTimeout(()=>{try{window.location.reload()}finally{this.isUpdating=!1}},this.silentMode?100:800))}savePageState(){try{sessionStorage.setItem("sw_scroll_position",window.scrollY.toString()),sessionStorage.setItem("sw_current_path",window.location.pathname+window.location.search),sessionStorage.setItem("sw_update_timestamp",Date.now().toString())}catch(error){console.error("[SW-AU] Failed to save page state:",error)}}restorePageState(){try{const scrollPos=sessionStorage.getItem("sw_scroll_position"),updateTime=sessionStorage.getItem("sw_update_timestamp");updateTime&&Date.now()-parseInt(updateTime)<5e3&&(scrollPos&&setTimeout(()=>{window.scrollTo(0,parseInt(scrollPos))},100),sessionStorage.removeItem("sw_scroll_position"),sessionStorage.removeItem("sw_current_path"),sessionStorage.removeItem("sw_update_timestamp"),this.silentMode||this.showUpdateCompleteIndicator())}catch(error){console.error("[SW-AU] Failed to restore page state:",error)}}showUpdateIndicator(){const indicator=this.createIndicator("正在检测更新...","update-checking");document.body.appendChild(indicator),setTimeout(()=>{indicator.parentNode&&indicator.parentNode.removeChild(indicator)},2e3)}showReloadIndicator(){const indicator=this.createIndicator("正在更新应用...","update-loading");document.body.appendChild(indicator)}showUpdateCompleteIndicator(){const indicator=this.createIndicator("应用已更新完成","update-complete",!0);document.body.appendChild(indicator),setTimeout(()=>{indicator.parentNode&&indicator.parentNode.removeChild(indicator)},3e3)}createIndicator(text,className,isSuccess=!1){const indicator=document.createElement("div");if(indicator.className=`sw-update-indicator ${className}`,indicator.innerHTML=`\n            <div class="indicator-content">\n                <div class="indicator-icon">\n                    ${isSuccess?'<i class="fas fa-check-circle"></i>':'<i class="fas fa-sync-alt fa-spin"></i>'}\n                </div>\n                <div class="indicator-text">${text}</div>\n            </div>\n        `,!document.getElementById("sw-update-styles")){const style=document.createElement("style");style.id="sw-update-styles",style.textContent="\n                .sw-update-indicator {\n                    position: fixed;\n                    top: 20px;\n                    right: 20px;\n                    background: rgba(0, 0, 0, 0.9);\n                    color: white;\n                    padding: 12px 16px;\n                    border-radius: 8px;\n                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n                    z-index: 10000;\n                    font-size: 14px;\n                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n                    opacity: 0;\n                    transform: translateY(-10px);\n                    transition: all 0.3s ease;\n                }\n                .sw-update-indicator.show {\n                    opacity: 1;\n                    transform: translateY(0);\n                }\n                .indicator-content {\n                    display: flex;\n                    align-items: center;\n                    gap: 8px;\n                }\n                .update-complete {\n                    background: rgba(34, 197, 94, 0.9);\n                }\n                @media (max-width: 480px) {\n                    .sw-update-indicator {\n                        top: 10px;\n                        right: 10px;\n                        left: 10px;\n                        font-size: 13px;\n                    }\n                }\n            ",document.head.appendChild(style)}return setTimeout(()=>indicator.classList.add("show"),10),indicator}startPeriodicCheck(){setInterval(()=>{this.checkVersion()},this.checkInterval)}handleCheckError(){this.retryCount++,this.retryCount<this.maxRetries?(this.retryCount,this.maxRetries,setTimeout(()=>this.checkVersion(),5e3)):(console.error("[SW-AU] Max retries reached for version check"),this.retryCount=0)}async retryRegistration(){this.retryCount++,this.retryCount<this.maxRetries&&(this.retryCount,this.maxRetries,setTimeout(()=>this.init(),5e3))}async manualCheckUpdate(){await this.checkVersion()}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{window.swAutoUpdater=new ServiceWorkerAutoUpdater}):window.swAutoUpdater=new ServiceWorkerAutoUpdater,window.addEventListener("load",()=>{window.swAutoUpdater&&window.swAutoUpdater.restorePageState()}),window.checkForUpdates=()=>
1{if(window.swAutoUpdater)return window.swAutoUpdater.manualCheckUpdate()};

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.