diff --git a/component/auth.html b/component/auth.html index ceca494c8f08d5bef14c4f8f9f51b4e6ca584273..ea74a90deeafd00bf8a86273f9ea045a96d60120 100644 --- a/component/auth.html +++ b/component/auth.html @@ -1,5 +1,5 @@ - - diff --git a/component/room-history.html b/component/room-history.html new file mode 100644 index 0000000000000000000000000000000000000000..26fe8de9b9ddbc8a58a36e50570db246849ce663 --- /dev/null +++ b/component/room-history.html @@ -0,0 +1,16 @@ + diff --git a/component/window.js b/component/window.js index 9ab820e6abf165a2d504a2ca9102f142ad18c506..877be9ba46124d7d3774c7db1073725d47e3b3be 100644 --- a/component/window.js +++ b/component/window.js @@ -116,7 +116,7 @@ class MacWindow extends HTMLElement { /* default content styling */ .content { padding: 12px; - height: calc(100% - 12px); + height: calc(100% - 12px - 40px); overflow: auto; } @@ -125,16 +125,27 @@ class MacWindow extends HTMLElement { padding: 0; overflow: hidden; /* iframe handles its own scrolling */ } - :host([embedded]) ::slotted(iframe) { - width: 100%; - height: 100%; - border: 0; - display: block; - -webkit-overflow-scrolling: touch; - scrollbar-width: thin; - } - /* try to hide iframe element scrollbars in webkit (note: doesn't change inner page scrollbars) */ - :host([embedded]) ::slotted(iframe)::-webkit-scrollbar { display: none; } + :host([embedded]) ::slotted(iframe) { + width: 100%; + height: 100%; + border: 0; + display: block; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + } + /* try to hide iframe element scrollbars in webkit (note: doesn't change inner page scrollbars) */ + :host([embedded]) ::slotted(iframe)::-webkit-scrollbar { display: none; } + /* loading overlay appended as a slotted element */ + :host([embedded]) ::slotted(.window-loader) { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0,0,0,0.28); + z-index: 5; + pointer-events: none; + } /* small title style for demo */ ::slotted(h2) { @@ -145,6 +156,25 @@ class MacWindow extends HTMLElement { } ::slotted(p) { color: rgba(255,255,255,0.85); margin: 0; } + .titlebar { + height: 40px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + gap: 8px; + border-bottom: 1px solid rgba(255,255,255,0.04); + background: linear-gradient(180deg, rgba(0,0,0,0.12), rgba(0,0,0,0.06)); + z-index: 3; + } + .nav-left, .nav-right { display:flex; align-items:center; gap:6px; } + .nav-btn { background: rgba(255,255,255,0.04); border: none; color: #fff; padding:6px 8px; border-radius:6px; cursor:pointer } + .title-center { flex:1; display:flex; flex-direction:column; align-items:center; overflow:hidden } + .title-text { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:600 } + .address-text { font-size:0.75rem; opacity:0.8; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:70% } + .open-link { color: #aee; text-decoration:none; padding:4px 6px; border-radius:6px; background: rgba(255,255,255,0.02) } + .resizer { position: absolute; right: 6px; bottom: 6px; width: 18px; height: 18px; cursor: se-resize; z-index: 4; border-radius:4px; background: rgba(255,255,255,0.03) } + @media (max-width: 720px) { .stage { left: 50%; top: 50%; transform: translate(-50%, -50%) scale(1); } .window { border-radius: 10px; } @@ -157,10 +187,24 @@ class MacWindow extends HTMLElement {
`; @@ -180,6 +224,9 @@ class MacWindow extends HTMLElement { onKey: this._onKey.bind(this), onBackdropClick: this._onBackdropClick.bind(this) }; + this._resizing = false; + this._currentIframe = null; + this._currentUrl = ''; } connectedCallback() { @@ -189,8 +236,21 @@ class MacWindow extends HTMLElement { this._stage.addEventListener('pointermove', this._bound.onMove); this._stage.addEventListener('pointerleave', this._bound.onLeave); this._closeBtn.addEventListener('click', this._bound.onClose); + // support pointerdown for quicker response on touch devices + this._closeBtn.addEventListener('pointerdown', this._bound.onClose); this._backdrop.addEventListener('click', this._bound.onBackdropClick); + // wire up titlebar controls + this._backBtn = this.shadowRoot.querySelector('.nav-btn.back'); + this._forwardBtn = this.shadowRoot.querySelector('.nav-btn.forward'); + this._openLinkAnchor = this.shadowRoot.querySelector('.open-link'); + this._titleText = this.shadowRoot.querySelector('.title-text'); + this._addressText = this.shadowRoot.querySelector('.address-text'); + this._resizer = this.shadowRoot.querySelector('.resizer'); + if (this._backBtn) this._backBtn.addEventListener('click', ()=> this._navigate(-1)); + if (this._forwardBtn) this._forwardBtn.addEventListener('click', ()=> this._navigate(1)); + if (this._resizer) this._resizer.addEventListener('pointerdown', this._onResizerDown.bind(this)); + // keyboard this.addEventListener('keydown', this._bound.onKey); this.setAttribute('tabindex', '0'); @@ -207,7 +267,11 @@ class MacWindow extends HTMLElement { this._stage.removeEventListener('pointermove', this._bound.onMove); this._stage.removeEventListener('pointerleave', this._bound.onLeave); this._closeBtn.removeEventListener('click', this._bound.onClose); + this._closeBtn.removeEventListener('pointerdown', this._bound.onClose); this._backdrop.removeEventListener('click', this._bound.onBackdropClick); + if (this._backBtn) this._backBtn.removeEventListener('click', ()=> this._navigate(-1)); + if (this._forwardBtn) this._forwardBtn.removeEventListener('click', ()=> this._navigate(1)); + if (this._resizer) this._resizer.removeEventListener('pointerdown', this._onResizerDown); this.removeEventListener('keydown', this._bound.onKey); } @@ -291,6 +355,69 @@ class MacWindow extends HTMLElement { this._inner.style.transform = 'rotateX(0deg) rotateY(0deg) translateZ(0px)'; } + _onResizerDown(e){ + e.preventDefault(); + this._resizing = true; + const startX = e.clientX, startY = e.clientY; + const startRect = this._stage.getBoundingClientRect(); + const startW = startRect.width; + const startH = startRect.height; + const onMove = (ev)=>{ + const nw = Math.max(280, startW + (ev.clientX - startX)); + const nh = Math.max(220, startH + (ev.clientY - startY)); + this._stage.style.width = nw + 'px'; + this._stage.style.height = nh + 'px'; + }; + const onUp = (ev)=>{ + this._resizing = false; + window.removeEventListener('pointermove', onMove); + window.removeEventListener('pointerup', onUp); + // persist size + try{ + if (this._currentUrl) { + window._macWindowSizeCache = window._macWindowSizeCache || {}; + window._macWindowSizeCache[this._currentUrl] = window._macWindowSizeCache[this._currentUrl] || {}; + const rect = this._stage.getBoundingClientRect(); + window._macWindowSizeCache[this._currentUrl].w = Math.round(rect.width); + window._macWindowSizeCache[this._currentUrl].h = Math.round(rect.height); + } + }catch(e){} + }; + window.addEventListener('pointermove', onMove); + window.addEventListener('pointerup', onUp); + } + + _navigate(delta){ + if (!this._currentIframe) return; + try { + if (delta < 0) this._currentIframe.contentWindow.history.back(); else this._currentIframe.contentWindow.history.forward(); + } catch(e) { + try { this._currentIframe.contentWindow.postMessage({ type: 'hsn-navigate', delta }, '*'); } catch(e){} + } + } + + registerIframe(iframe, url){ + this._currentIframe = iframe; + this._currentUrl = url || ''; + if (this._openLinkAnchor) this._openLinkAnchor.href = url || ''; + const update = ()=>{ + try{ + const doc = iframe.contentDocument; + const title = doc && (doc.title || (doc.querySelector && doc.querySelector('h1') && doc.querySelector('h1').textContent)) || iframe.getAttribute('title') || ''; + if (this._titleText) this._titleText.textContent = title || '窗口'; + if (this._addressText) this._addressText.textContent = (iframe.contentWindow && iframe.contentWindow.location && iframe.contentWindow.location.href) || url || ''; + }catch(e){ + if (this._titleText) this._titleText.textContent = url || '窗口'; + if (this._addressText) this._addressText.textContent = url || ''; + } + try{ + if (this._backBtn) this._backBtn.disabled = !(iframe.contentWindow && iframe.contentWindow.history && iframe.contentWindow.history.length>1); + }catch(e){} + }; + iframe.addEventListener('load', ()=>{ update(); setTimeout(update,120); }); + setTimeout(update,120); + } + _applyResponsiveSizing() { // 移动端自适应 if (!this._stage) return; diff --git a/css/style.css b/css/style.css index 28a9953ac1f68d7d72708a4b598164c07ba0e3c1..52ee7adf3863670f62175a188df7a3450b7f5493 100644 --- a/css/style.css +++ b/css/style.css @@ -684,3 +684,84 @@ box-shadow: 0 6px 18px rgba(97,232,234,0.12); transform: none; } + +/* 统一表格组件样式(用于 rooms/ranks/top) */ +.table-card { + perspective: 1000px; + width: 95%; + max-width: 1200px; + margin: 0 auto; +} +.table-card .table-scroll { + position: relative; + width: 100%; + -webkit-overflow-scrolling: touch; +} + +/* 将滚动作用于 table 本身以满足视觉需求 */ +table.responsive-table { + display: block; /* allow native horizontal scrolling on table */ + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + white-space: nowrap; + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + backdrop-filter: blur(8px); + transform-style: preserve-3d; + transition: transform 0.18s ease; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18); + border: 1px solid rgba(255,255,255,0.14); +} + +/* 固定列样式,避免在窄屏下表格列内换行导致宽度异常 */ +table.responsive-table th, table.responsive-table td { + white-space: nowrap; +} + +/* 更合理的表格布局在小屏下取消 3D 位移并使用 fixed 布局 */ +@media (max-width: 768px) { + table.responsive-table { + transform: none !important; + animation: none !important; + table-layout: fixed; + } + table.responsive-table th, table.responsive-table td { + padding: 0.5rem 0.6rem; + font-size: 0.85rem; + text-overflow: ellipsis; + overflow: hidden; + } +} + +/* 细窄的自定义滚动条(作用于 table 元素) */ +table.responsive-table::-webkit-scrollbar { + height: 8px; +} +table.responsive-table::-webkit-scrollbar-track { + background: rgba(255,255,255,0.03); + border-radius: 8px; +} +table.responsive-table::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.12); + border-radius: 8px; + border: 1px solid rgba(255,255,255,0.06); +} +/* Firefox */ +table.responsive-table { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.12) rgba(255,255,255,0.03); } + +/* 长文本滚动处理(用于谱面/长标题) */ +.scrolling-btn{ display:inline-block; max-width:150px; overflow:hidden; position:relative; vertical-align:middle; } +.scrolling-btn > span{ display:inline-block; padding-right:24px; will-change:transform; } +.scrolling-btn.marquee > span{ animation: scroll-left 8s linear infinite; } +@keyframes scroll-left{ + 0%{ transform: translateX(0); } + 100%{ transform: translateX(-100%); } +} + +/* 当容器较窄时启用更温和的滚动 */ +@media (max-width: 768px){ + .scrolling-btn{ max-width:120px; } + .scrolling-btn.marquee > span{ animation-duration:10s; } +} + diff --git a/index.html b/index.html index 68d78697d672d27a54b62997a434364241f17e54..b82735179c6d1d20b352203fe1b374ce104b56b2 100644 --- a/index.html +++ b/index.html @@ -11,19 +11,14 @@ - - -
- logo - -
+ + + +
操作成功!
diff --git a/js/agreement_update.js b/js/agreement_update.js new file mode 100644 index 0000000000000000000000000000000000000000..6ec691642ce262434ca9d88a3cd28b3fe37ca95b --- /dev/null +++ b/js/agreement_update.js @@ -0,0 +1,21 @@ +// Agreement update notifier +(function(){ + const KEY = '__hsn_last_agreement_v'; + async function check(){ + try{ + const r = await fetch('/api/agreements/meta', {cache: 'no-store'}); + if (!r.ok) return; + const d = await r.json(); + const v = d.version || d.updated_at || ''; + const prev = localStorage.getItem(KEY); + if (prev && v && prev !== v){ + if (typeof showMessage === 'function'){ + showMessage('服务条款更新', '服务条款已更新,请查看并同意', 'linear-gradient(135deg, rgba(255,192,32,0.08), rgba(255,96,96,0.06))', 0); + } + } + if (v) localStorage.setItem(KEY, v); + }catch(e){} + } + check(); + setInterval(check, 1000*60*30); +})(); diff --git a/js/auth-window.js b/js/auth-window.js new file mode 100644 index 0000000000000000000000000000000000000000..97de41b6dd54875fe12586406d2d1df280af8e20 --- /dev/null +++ b/js/auth-window.js @@ -0,0 +1,136 @@ +(function(){ + // Centralized auth window using mac-window + const state = { authMode: 'login', win: null }; + + function createWindowIfNeeded(){ + if (state.win && document.body.contains(state.win)) return state.win; + // create a mac-window instance + const win = document.createElement('mac-window'); + win.id = 'auth-mac-window'; + // smaller default + win.style.setProperty('--width','420px'); + win.style.setProperty('--height','380px'); + document.body.appendChild(win); + state.win = win; + + // when closed remove instance so next open creates fresh one + win.addEventListener('mac-window-close', () => { + try { win.remove(); } catch(e){} + state.win = null; + }); + + return win; + } + + function openAuth(){ + const tmpl = document.getElementById('auth-template'); + if (!tmpl) return console.warn('auth template missing'); + const win = createWindowIfNeeded(); + + // clear previous content + const existing = win.querySelector('#auth-box'); + if (existing) existing.remove(); + + const node = tmpl.content.cloneNode(true); + // append nodes into window (slot area) + win.appendChild(node); + + // wire up buttons + const submitBtn = win.querySelector('#auth-submit'); + const toggleBtn = win.querySelector('#auth-toggle'); + const msgElem = win.querySelector('#auth-msg'); + + if (submitBtn) submitBtn.addEventListener('click', submitAuth); + if (toggleBtn) toggleBtn.addEventListener('click', toggleAuthMode); + + // ensure UI initial state + updateAuthUI(); + + win.open(); + } + + function closeAuth(){ + if (state.win) state.win.close(); + } + + function toggleAuthMode(){ + state.authMode = state.authMode === 'login' ? 'register' : 'login'; + updateAuthUI(); + } + + function updateAuthUI(){ + const win = state.win || document.getElementById('auth-mac-window'); + const title = win && win.querySelector('#auth-title'); + const phiraid = win && win.querySelector('#auth-phiraid'); + const agreementContainer = win && win.querySelector('#agreement-container'); + if (!title) return; + if (state.authMode === 'login'){ + title.textContent = '用户登录'; + if (phiraid) phiraid.classList.add('collapsed'); + if (agreementContainer) agreementContainer.style.display = 'none'; + } else { + title.textContent = '用户注册'; + if (phiraid) phiraid.classList.remove('collapsed'); + if (agreementContainer) agreementContainer.style.display = 'flex'; + } + } + + async function submitAuth(){ + const win = state.win || document.getElementById('auth-mac-window'); + if (!win) return; + const username = win.querySelector('#auth-name').value; + const password = win.querySelector('#auth-password').value; + const phira_id = win.querySelector('#auth-phiraid').value; + const remember = !!(win.querySelector('#remember-me') && win.querySelector('#remember-me').checked); + const agreeTerms = state.authMode === 'register' ? !!(win.querySelector('#agree-terms') && win.querySelector('#agree-terms').checked) : true; + const msg = win.querySelector('#auth-msg'); + if (msg) msg.textContent = '处理中...'; + + try { + if (state.authMode === 'register' && !agreeTerms) { + if (msg) msg.textContent = '请同意用户协议'; + return; + } + const endpoint = state.authMode === 'login' ? '/api/auth/login' : '/api/auth/users'; + const payload = state.authMode === 'login' ? { username, password, remember } : { username, password, phira_id }; + const res = await fetch(endpoint, { + method: 'POST', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify(payload) + }); + if (!res.ok) { + const err = await res.json().catch(()=>({})); + if (msg) msg.textContent = err.message || '操作失败'; + return; + } + if (state.authMode === 'login'){ + // refresh global currentUser if applicable + try { + const userRes = await fetch('/api/auth/me'); + if (userRes.ok) { + window.currentUser = await userRes.json(); + if (window.updateUserDisplay) try { window.updateUserDisplay(); } catch(e){} + } + } catch(e){} + } + if (msg) msg.textContent = state.authMode === 'login' ? '登录成功!' : '注册成功!'; + setTimeout(()=>{ + try { closeAuth(); } catch(e){} + if (state.authMode === 'register') state.authMode = 'login'; + }, 800); + } catch (e) { + if (msg) msg.textContent = '网络错误'; + console.error('auth error', e); + } + } + + // expose globals used by header and other scripts + window.openAuth = openAuth; + window.closeAuth = closeAuth; + window.toggleAuthMode = toggleAuthMode; + window.submitAuth = submitAuth; + window.updateAuthUI = updateAuthUI; + + // auto-initialize if template not yet loaded: wait for DOMContentLoaded + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ()=>{}); +})(); diff --git a/js/auto_update.js b/js/auto_update.js new file mode 100644 index 0000000000000000000000000000000000000000..a8f53eadd20cf1510d5a6b453e3ec27d32f51fd1 --- /dev/null +++ b/js/auto_update.js @@ -0,0 +1,22 @@ +// Simple auto-update checker +(function(){ + const CHECK_INTERVAL = 1000 * 60 * 5; // 5 minutes + async function check(){ + try{ + const r = await fetch('/api/version', {cache: 'no-store'}); + if (!r.ok) return; + const data = await r.json(); + if (!window.__hsn_last_version) window.__hsn_last_version = data.version; + if (data.version && data.version !== window.__hsn_last_version){ + window.__hsn_last_version = data.version; + if (typeof showMessage === 'function'){ + showMessage('更新可用', '检测到新版本,建议刷新页面以获取最新内容', 'linear-gradient(135deg, rgba(32,160,255,0.12), rgba(0,200,120,0.08))', 0); + } + } + }catch(e){ + // ignore + } + } + check(); + setInterval(check, CHECK_INTERVAL); +})(); diff --git a/js/contact-window.js b/js/contact-window.js new file mode 100644 index 0000000000000000000000000000000000000000..e124983da715344a3ef2734556aa4bfa64161797 --- /dev/null +++ b/js/contact-window.js @@ -0,0 +1,44 @@ +// Contact window controller +(function(){ + async function openContact(){ + const tplResp = await fetch('./component/contact.html'); + const text = await tplResp.text(); + const div = document.createElement('div'); + div.innerHTML = text; + const tpl = div.querySelector('#contact-template'); + if (!tpl) return; + + const win = document.createElement('mac-window'); + win.style.setProperty('--width','640px'); + win.style.setProperty('--height','420px'); + document.body.appendChild(win); + + const content = tpl.content.cloneNode(true); + const slotWrap = document.createElement('div'); + slotWrap.appendChild(content); + win.appendChild(slotWrap); + + // simple carousel dummy: try to fetch announcements + try{ + const r = await fetch('/api/announcements?limit=5'); + if (r.ok){ + const arr = await r.json(); + const carousel = win.querySelector('#contact-carousel'); + if (carousel && Array.isArray(arr)){ + carousel.innerHTML = ''; + arr.forEach(a=>{ + const it = document.createElement('div'); + it.className = 'contact-item'; + it.textContent = a.title || a.summary || '公告'; + carousel.appendChild(it); + }); + } + } + }catch(e){} + + // expose for global usage + window.openContact = openContact; + } + + window.openContact = openContact; +})(); diff --git a/js/message.js b/js/message.js new file mode 100644 index 0000000000000000000000000000000000000000..6cddc4cedb87ebad0d5641db74ce6756d78515f3 --- /dev/null +++ b/js/message.js @@ -0,0 +1,70 @@ +// Unified message component +(function(){ + const containerId = 'hsn-message-container'; + function ensureContainer(){ + let c = document.getElementById(containerId); + if (!c){ + c = document.createElement('div'); + c.id = containerId; + c.style.position = 'fixed'; + c.style.top = '12px'; + c.style.right = '12px'; + c.style.zIndex = 200000; + c.style.display = 'flex'; + c.style.flexDirection = 'column'; + c.style.gap = '8px'; + document.body.appendChild(c); + } + return c; + } + + function showMessage(title, content, bgColor, timeout=4000){ + const c = ensureContainer(); + const card = document.createElement('div'); + card.style.minWidth = '220px'; + card.style.maxWidth = '420px'; + card.style.background = bgColor || 'rgba(255,255,255,0.06)'; + card.style.backdropFilter = 'blur(12px)'; + card.style.border = '1px solid rgba(255,255,255,0.08)'; + card.style.borderRadius = '12px'; + card.style.padding = '12px'; + card.style.color = '#fff'; + card.style.boxShadow = '0 8px 32px rgba(0,0,0,0.3)'; + card.style.position = 'relative'; + + const close = document.createElement('button'); + close.textContent = '×'; + close.style.position = 'absolute'; + close.style.right = '8px'; + close.style.top = '8px'; + close.style.border = 'none'; + close.style.background = 'rgba(255,0,0,0.7)'; + close.style.color = '#fff'; + close.style.width = '20px'; + close.style.height = '20px'; + close.style.borderRadius = '50%'; + close.style.cursor = 'pointer'; + close.addEventListener('click', ()=>{ if (card.parentNode) card.parentNode.removeChild(card); }); + + const h = document.createElement('div'); + h.textContent = title || ''; + h.style.fontSize = '16px'; + h.style.fontWeight = '700'; + h.style.marginBottom = '6px'; + + const p = document.createElement('div'); + p.innerHTML = content || ''; + p.style.fontSize = '13px'; + + card.appendChild(close); + card.appendChild(h); + card.appendChild(p); + c.appendChild(card); + + if (timeout > 0){ + setTimeout(()=>{ try{ if (card.parentNode) card.parentNode.removeChild(card); }catch(e){} }, timeout); + } + } + + window.showMessage = showMessage; +})(); diff --git a/js/room-history.js b/js/room-history.js new file mode 100644 index 0000000000000000000000000000000000000000..d20206f4b717d31e7884a818d481889f60d11eb9 --- /dev/null +++ b/js/room-history.js @@ -0,0 +1,58 @@ +// Room play-history window +(function(){ + async function openRoomHistory(roomId){ + if (!roomId) { showMessage && showMessage('错误','未提供房间标识'); return; } + + // load template + let tplText = ''; + try{ + const r = await fetch('./component/room-history.html'); + tplText = await r.text(); + }catch(e){ tplText = ''; } + const wrapper = document.createElement('div'); + wrapper.innerHTML = tplText; + const tpl = wrapper.querySelector('#room-history-template'); + if (!tpl) { showMessage && showMessage('错误','无法加载历史模板'); return; } + + const win = document.createElement('mac-window'); + win.style.setProperty('--width','680px'); + win.style.setProperty('--height','420px'); + document.body.appendChild(win); + + const content = tpl.content.cloneNode(true); + const container = document.createElement('div'); + container.appendChild(content); + win.appendChild(container); + + const meta = win.querySelector('#history-meta'); + const tbody = win.querySelector('#history-table tbody'); + if (meta) meta.textContent = `房间:${roomId}`; + + // try multiple endpoints + const candidates = [`/api/rooms/history/${encodeURIComponent(roomId)}`, `/api/rooms/history?room=${encodeURIComponent(roomId)}`]; + let data = null; + for (const url of candidates){ + try{ + const r = await fetch(url); + if (!r.ok) continue; + data = await r.json(); + break; + }catch(e){} + } + + if (!Array.isArray(data) || data.length === 0){ + tbody.innerHTML = '暂无历史记录'; + return; + } + + tbody.innerHTML = data.map(item => { + const player = item.player_name || item.player || item.user || '匿名'; + const chart = item.chart_name || item.chart || item.song || '未知'; + const score = item.score != null ? item.score : (item.result || '—'); + const t = item.time || item.ts || item.played_at || ''; + return `${player}${chart}${score}${t}`; + }).join(''); + } + + window.openRoomHistory = openRoomHistory; +})(); diff --git a/js/rooms.js b/js/rooms.js index c6763e39d5055608a3ccae4cded9d4be2fe6526f..bd4fcbc92e6b0fbb9ce3c6ff5962d93bc8a468e8 100644 --- a/js/rooms.js +++ b/js/rooms.js @@ -149,13 +149,13 @@ const chartInfo = await getChartInfo(room.chart); if (chartInfo) { chartText = chartInfo.name - ? `${chartInfo.name}` + ? `${chartInfo.name}` : `ID: ${room.chart}`; chartImg = chartInfo.illustration ? `` : "无封面"; downloadBtn = chartInfo.file - ? `` + ? `` : "无文件"; } } @@ -164,6 +164,8 @@ const stateDisplay = `${stateInfo.text}`; const hostBtn = ``; const usersBtn = ``; + const roomIdVal = (room.id || room.name || '').toString().replace(/'/g, "\\'"); + const historyBtn = ``; const tr = document.createElement("tr"); tr.innerHTML = ` ${room.name} @@ -175,6 +177,7 @@ ${chartImg} ${downloadBtn} ${usersBtn} + ${historyBtn} `; tbody.appendChild(tr); } diff --git a/js/table-component.js b/js/table-component.js new file mode 100644 index 0000000000000000000000000000000000000000..8f2baf9d98fe967c112899b02a2ef4ff56b3e26e --- /dev/null +++ b/js/table-component.js @@ -0,0 +1,57 @@ +// Shared table helper: adds touch-friendly horizontal scroll behavior +// and optional sticky header handling. Auto-initializes tables with class +// "responsive-table". +(function(){ + function initTable(t){ + if (t.__tableComponentInit) return; t.__tableComponentInit = true; + // make sure the table uses block display for native horizontal scroll + t.style.display = 'block'; + t.style.overflowX = 'auto'; + t.style.webkitOverflowScrolling = 'touch'; + + // add wheel-to-scroll-on-x for desktop mousewheel when shift not held + t.addEventListener('wheel', function(e){ + if (Math.abs(e.deltaX) < Math.abs(e.deltaY)){ + // translate vertical wheel to horizontal scroll for convenience + t.scrollLeft += e.deltaY; + e.preventDefault(); + } + }, { passive: false }); + + // make header visually sticky by cloning header row into a fixed element if desired + // keep minimal: add shadow when scrolled horizontally to hint overflow + function updateShadow(){ + if (t.scrollLeft > 0) t.style.boxShadow = 'inset 8px 0 12px -8px rgba(0,0,0,0.4)'; + else t.style.boxShadow = ''; + } + t.addEventListener('scroll', updateShadow); + updateShadow(); + + // accessibility: expose keyboard horizontal navigation + t.setAttribute('tabindex','0'); + t.addEventListener('keydown', function(e){ + if (e.key === 'ArrowRight') { t.scrollLeft += 60; e.preventDefault(); } + if (e.key === 'ArrowLeft') { t.scrollLeft -= 60; e.preventDefault(); } + if (e.key === 'Home') { t.scrollLeft = 0; } + if (e.key === 'End') { t.scrollLeft = t.scrollWidth; } + }); + } + + function scan(){ + document.querySelectorAll('table.responsive-table').forEach(initTable); + // enable marquee for long scrolling-btn elements + setTimeout(()=>{ + document.querySelectorAll('.scrolling-btn').forEach(el=>{ + try{ + const span = el.querySelector('span'); + if (span && span.scrollWidth > el.clientWidth) el.classList.add('marquee'); + }catch(e){} + }); + }, 120); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan); else scan(); + + // expose for manual init + window.HSNTable = { init: initTable }; +})(); diff --git a/js/top.js b/js/top.js index 1fc432fee7a82d488136ac7d4ed8f70c217d9446..222f1a3eab4977160707ca0f0e983a3ce4b752a8 100644 --- a/js/top.js +++ b/js/top.js @@ -216,12 +216,20 @@ // 谱面名称列 const nameTd = document.createElement("td"); - if (chartInfo && chartInfo.name) { + if (chartInfo && chartInfo.name) { const nameBtn = document.createElement("a"); nameBtn.href = `https://phira.moe/chart/${chart.chart_id}`; nameBtn.target = "_blank"; - nameBtn.className = "chart-name-btn"; - nameBtn.textContent = chartInfo.name; + nameBtn.className = "chart-name-btn scrolling-btn"; + const innerSpan = document.createElement('span'); + innerSpan.textContent = chartInfo.name; + nameBtn.appendChild(innerSpan); + // if text overflows, add marquee class to animate + setTimeout(()=>{ + try{ + if (innerSpan.scrollWidth > nameBtn.clientWidth) nameBtn.classList.add('marquee'); + }catch(e){} + }, 120); nameTd.appendChild(nameBtn); } else { nameTd.textContent = `ID: ${chart.chart_id}`; @@ -267,10 +275,12 @@ // 下载列 const downloadTd = document.createElement("td"); if (chartInfo && chartInfo.file) { - const downloadLink = document.createElement("a"); + const downloadLink = document.createElement('a'); downloadLink.href = chartInfo.file; - downloadLink.download = `chart_${chart.chart_id}.zip`; - const downloadBtn = document.createElement("button"); + // use chart name if available, sanitize filename + const safeName = (chartInfo.name || `chart_${chart.chart_id}`).replace(/[^\w\-\u4e00-\u9fa5\. ]+/g, '').trim(); + downloadLink.download = `${safeName || 'chart_' + chart.chart_id}.zip`; + const downloadBtn = document.createElement('button'); downloadBtn.className = "dl-btn"; downloadBtn.textContent = "下载"; downloadLink.appendChild(downloadBtn); @@ -287,22 +297,27 @@ updatePaginationControls(); } - // 复制谱面ID - function copyChartId(chartId) { - navigator.clipboard.writeText(chartId) - .then(() => { - // 显示复制成功提示 - const notification = document.getElementById('copy-notification'); - notification.classList.add('show'); - setTimeout(() => { - notification.classList.remove('show'); - }, 2000); - }) - .catch(err => { - console.error('复制失败:', err); - alert('复制失败,请手动复制谱面ID'); - }); - } + // 复制谱面ID(前置#) + function copyChartId(chartId) { + const idText = '#' + String(chartId); + navigator.clipboard.writeText(idText) + .then(() => { + if (window.showMessage) { + window.showMessage('已复制', idText, ''); + } else { + const notification = document.getElementById('copy-notification'); + if (notification) { + notification.textContent = `谱面ID ${idText} 已复制到剪贴板!`; + notification.classList.add('show'); + setTimeout(() => notification.classList.remove('show'), 2000); + } + } + }) + .catch(err => { + console.error('复制失败:', err); + alert('复制失败,请手动复制谱面ID'); + }); + } // 更新分页控件状态 function updatePaginationControls() { diff --git a/js/window-links.js b/js/window-links.js index d3018e0022eaeb54cd018fb63c8cd96f7b818787..721ca77f547d5567b2c9535279e912100465d601 100644 --- a/js/window-links.js +++ b/js/window-links.js @@ -8,6 +8,11 @@ if (/^\s*javascript:/i.test(href)) return false; // treat privacy page specially if (/privacy\.html(\b|$)/i.test(href)) return true; + // if link points to a downloadable file, do not treat as external page + const downloadExt = ['.zip', '.osz', '.osu', '.png', '.jpg', '.jpeg', '.gif', '.mp3', '.ogg', '.7z']; + for (let ext of downloadExt) { + if (href.toLowerCase().split('?')[0].endsWith(ext)) return false; + } // protocol-relative or absolute if (/^\/\//.test(href)) return true; if (/^https?:\/\//i.test(href)) { @@ -20,43 +25,67 @@ } } - function ensureMacWindow() { - let win = document.getElementById('global-mac-window'); - if (!win) { - win = document.createElement('mac-window'); - win.id = 'global-mac-window'; - // set a sensible default size - win.style.setProperty('--width','900px'); - win.style.setProperty('--height','640px'); - document.body.appendChild(win); + // create a new mac-window instance and wire stacking/cleanup + function createMacWindow() { + window._macWindowCounter = (window._macWindowCounter || 0) + 1; + window._macWindowZ = window._macWindowZ || 100001; + window._macWindowSizeCache = window._macWindowSizeCache || {}; + const id = 'global-mac-window-' + window._macWindowCounter; + const win = document.createElement('mac-window'); + win.id = id; + // bring this window to front by assigning z + win.style.setProperty('--mac-window-z', (window._macWindowZ++).toString()); + // default size + win.style.setProperty('--width','900px'); + win.style.setProperty('--height','640px'); - // cleanup iframe on close - win.addEventListener('mac-window-close', () => { - const iframe = win.querySelector('iframe'); - if (iframe) iframe.src = 'about:blank'; - // remove embedded mode and any custom sizing - win.removeAttribute('embedded'); - win.style.removeProperty('--width'); - win.style.removeProperty('--height'); - // remove slotted title if any - const title = win.querySelector('h2[data-window-title]'); - if (title) title.remove(); - }); - } + // clicking/touching the window brings it to front + win.addEventListener('pointerdown', () => { + win.style.setProperty('--mac-window-z', (window._macWindowZ++).toString()); + }); + + // cleanup on close: remove element entirely + const onClose = () => { + const iframe = win.querySelector('iframe'); + try { if (iframe) iframe.src = 'about:blank'; } catch(e){} + // remove loader if any + const loader = win.querySelector('.window-loader'); + try { if (loader && loader.remove) loader.remove(); } catch(e){} + // finally remove element + try { win.remove(); } catch(e){} + }; + win.addEventListener('mac-window-close', onClose); + + document.body.appendChild(win); return win; } function openUrlInWindow(url, title, width, height) { - const win = ensureMacWindow(); + const win = createMacWindow(); // remove existing iframe to avoid duplicates let iframe = win.querySelector('iframe'); if (iframe) iframe.remove(); // embedded mode: remove title and let iframe fill the window win.setAttribute('embedded',''); + + // if no explicit width/height provided, check cached size for this URL + window._macWindowSizeCache = window._macWindowSizeCache || {}; + const key = url; + if (!width && window._macWindowSizeCache[key]) { + const s = window._macWindowSizeCache[key]; + if (s.w) win.style.setProperty('--width', s.w + 'px'); + if (s.h) win.style.setProperty('--height', s.h + 'px'); + } if (width) win.style.setProperty('--width', width); if (height) win.style.setProperty('--height', height); + // add a loading overlay while iframe loads + const loader = document.createElement('div'); + loader.className = 'window-loader'; + loader.innerHTML = '
'; + win.appendChild(loader); + iframe = document.createElement('iframe'); iframe.src = url; iframe.setAttribute('tabindex','0'); @@ -66,16 +95,48 @@ iframe.style.height = '100%'; iframe.style.border = '0'; iframe.style.minHeight = '320px'; - // make iframe interactive and allow fullscreen/clipboard when possible iframe.setAttribute('allow','fullscreen; geolocation; microphone; camera; clipboard-read; clipboard-write'); - // some pages may want to be narrower; allow link to specify data-window-width/height + + // remove loader once iframe finishes loading; attempt to cache size when same-origin + iframe.addEventListener('load', () => { + try { if (loader && loader.remove) loader.remove(); } catch(e){} + // try measure content size if same-origin + try { + const doc = iframe.contentDocument || iframe.contentWindow.document; + if (doc) { + const fullH = Math.max(doc.documentElement.scrollHeight, doc.body.scrollHeight); + const fullW = Math.max(doc.documentElement.scrollWidth, doc.body.scrollWidth); + // only cache reasonable sizes + if (fullH && fullH < 3000) { + window._macWindowSizeCache[key] = window._macWindowSizeCache[key] || {}; + window._macWindowSizeCache[key].h = fullH; + window._macWindowSizeCache[key].w = fullW; + // apply cached height to window for better fit (desktop) + win.style.setProperty('--height', Math.max(320, fullH) + 'px'); + } + } + } catch(e) { + // cross-origin — ignore + } + // try inject minimal reset CSS into same-origin iframe to avoid body margin gaps + try { + const doc = iframe.contentDocument || iframe.contentWindow.document; + if (doc) { + const style = doc.createElement('style'); + style.textContent = 'html,body{height:100%;margin:0;padding:0;background:transparent;}body>*{box-sizing:border-box;}'; + doc.head && doc.head.appendChild(style); + } + } catch(e) { + // cross-origin: cannot inject + } + setTimeout(() => { try { iframe.contentWindow && iframe.contentWindow.focus(); } catch(e){} }, 120); + }, { once: true }); + win.appendChild(iframe); + // let mac-window manage title/navigation if supported + try { if (typeof win.registerIframe === 'function') win.registerIframe(iframe, url); } catch(e){} win.open(); - // focus for accessibility - setTimeout(() => { - try { iframe.contentWindow && iframe.contentWindow.focus(); } catch(e){} - }, 300); } // delegate click handler diff --git a/privacy.html b/privacy.html index 586d65024bc3ff7dca6183de9df5830525103f97..8713f04d596a5d69928e3d159e4cb65261c5b728 100644 --- a/privacy.html +++ b/privacy.html @@ -148,7 +148,7 @@

1. 协议变更

diff --git a/ranks.html b/ranks.html index 3cd7720dd0148df3a134ec582959af0afa15735b..fec106a18193616c77d3ecf990ce0cba8d3e4d19 100644 --- a/ranks.html +++ b/ranks.html @@ -47,18 +47,22 @@
- - - - - - - - - - - -
名次用户名游玩时间
加载中...
+
+
+ + + + + + + + + + + +
名次用户名游玩时间
加载中...
+
+
- + + + diff --git a/rooms.html b/rooms.html index 580181ba1fd07755fbf7f83e6c047c198dbdbb17..2c077fb61b5f67bf18918f736fd429d2e3c7a822 100644 --- a/rooms.html +++ b/rooms.html @@ -37,24 +37,29 @@
服务器状态:检测中...
- - - - - - - - - - - - - - - - - -
房间名房主人数状态循环谱面曲绘下载人员
加载中...
+
+
+ + + + + + + + + + + + + + + + + + +
房间名房主人数状态循环谱面曲绘下载人员历史
加载中...
+
+
@@ -68,6 +73,9 @@ + + + diff --git a/top.html b/top.html index bea33762dd319db93f1bee7d005b6b854f586400..00a6d1386069e6e0d1146a5c47e6fd2a1f7a8d28 100644 --- a/top.html +++ b/top.html @@ -45,21 +45,25 @@
- - - - - - - - - - - - - - -
名次谱面名称谱面ID游玩人数曲绘下载
加载中...
+
+
+ + + + + + + + + + + + + + +
名次谱面名称谱面ID游玩人数曲绘下载
加载中...
+
+
@@ -82,7 +86,9 @@ + + \ No newline at end of file