chore: 初始提交 - 移除所有硬编码凭据,统一从环境变量读取
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
let WALL_CONF = { autoplay_ms: 45000, swipe_pause_ms: 5 * 60 * 1000, max_cards_on_screen: 2 }
|
||||
let SCENES = []
|
||||
let editingCardId = null
|
||||
|
||||
const elCards = () => document.getElementById('cards')
|
||||
const elStatus = () => document.getElementById('status')
|
||||
|
||||
let carouselTimer = null
|
||||
let pausedUntil = 0
|
||||
let currentIndex = 0
|
||||
|
||||
const fmtTime = d => (window.fmtCNSimpleTime ? window.fmtCNSimpleTime(d) : `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`)
|
||||
const dayIndex = () => Math.floor((Date.now() / (24*3600*1000))) % 3
|
||||
|
||||
const buildCardEl = (c) => {
|
||||
const card = document.createElement('div')
|
||||
card.className = 'card'
|
||||
const title = document.createElement('div')
|
||||
title.className = 'title'
|
||||
title.textContent = c.title
|
||||
const lines = document.createElement('div')
|
||||
lines.className = 'lines'
|
||||
for (const s of c.items) { const ln = document.createElement('div'); ln.textContent = s; lines.appendChild(ln) }
|
||||
const meta = document.createElement('div')
|
||||
meta.className = 'meta'
|
||||
meta.textContent = c.meta
|
||||
card.appendChild(title)
|
||||
card.appendChild(lines)
|
||||
card.appendChild(meta)
|
||||
return card
|
||||
}
|
||||
|
||||
const renderCards = (mode = 'normal') => {
|
||||
const box = elCards(); if (!box) return
|
||||
let inner = document.getElementById('cards-inner')
|
||||
if (!inner) { inner = document.createElement('div'); inner.id = 'cards-inner'; inner.className = 'cards-inner'; box.innerHTML = ''; box.appendChild(inner) } else { inner.innerHTML = '' }
|
||||
const idx = currentIndex
|
||||
if (mode === 'prevPrep') {
|
||||
const prev = SCENES[(idx - 1 + SCENES.length) % SCENES.length]
|
||||
const cur = SCENES[idx % SCENES.length]
|
||||
const next = SCENES[(idx + 1) % SCENES.length]
|
||||
inner.appendChild(buildCardEl(prev))
|
||||
inner.appendChild(buildCardEl(cur))
|
||||
inner.appendChild(buildCardEl(next))
|
||||
inner.style.transform = 'translateX(-50%)'
|
||||
} else if (mode === 'nextPrep') {
|
||||
const cur = SCENES[idx % SCENES.length]
|
||||
const next = SCENES[(idx + 1) % SCENES.length]
|
||||
const next2 = SCENES[(idx + 2) % SCENES.length]
|
||||
inner.appendChild(buildCardEl(cur))
|
||||
inner.appendChild(buildCardEl(next))
|
||||
inner.appendChild(buildCardEl(next2))
|
||||
inner.style.transform = 'translateX(0)'
|
||||
} else {
|
||||
const cur = SCENES[idx % SCENES.length]
|
||||
const next = SCENES[(idx + 1) % SCENES.length]
|
||||
inner.appendChild(buildCardEl(cur))
|
||||
inner.appendChild(buildCardEl(next))
|
||||
inner.style.transform = 'translateX(0)'
|
||||
}
|
||||
updatePreviewActive()
|
||||
fitLayout()
|
||||
}
|
||||
|
||||
const rotate = () => { currentIndex = (currentIndex + 1) % SCENES.length; renderCards(); updatePreviewActive() }
|
||||
const rotatePrev = () => { currentIndex = (currentIndex - 1 + SCENES.length) % SCENES.length; renderCards(); updatePreviewActive() }
|
||||
|
||||
const updatePreviewActive = () => {
|
||||
const box = document.getElementById('preview'); if (!box) return
|
||||
box.querySelectorAll('.thumb').forEach(node => {
|
||||
const i = parseInt(node.getAttribute('data-idx') || '0', 10)
|
||||
if (i === currentIndex) node.classList.add('active'); else node.classList.remove('active')
|
||||
})
|
||||
}
|
||||
|
||||
const startAutoplay = () => {
|
||||
if (carouselTimer) clearInterval(carouselTimer)
|
||||
const status = elStatus(); if (status) { status.style.display = 'none'; status.textContent = '' }
|
||||
carouselTimer = setInterval(() => {
|
||||
if (Date.now() < pausedUntil) return
|
||||
rotate()
|
||||
}, WALL_CONF.autoplay_ms)
|
||||
}
|
||||
|
||||
const pauseAutoplay = ms => { pausedUntil = Date.now() + ms; const status = elStatus(); if (status) { status.style.display = ''; const t = new Date(pausedUntil); status.textContent = `已暂停自动播放,至 ${fmtTime(t)}` } }
|
||||
|
||||
const bindSwipe = () => {
|
||||
const area = elCards(); if (!area) return
|
||||
let x0 = 0, y0 = 0, dragging = false
|
||||
area.addEventListener('touchstart', (e) => { const t = e.touches[0]; x0 = t.clientX; y0 = t.clientY; dragging = true; const inner = document.getElementById('cards-inner'); if (inner) inner.style.transition = 'none' }, { passive: true })
|
||||
area.addEventListener('touchmove', (e) => { if (!dragging) return; const t = e.touches[0]; const dx = t.clientX - x0; const dy = t.clientY - y0; if (Math.abs(dx) > Math.abs(dy)) { const inner = document.getElementById('cards-inner'); if (inner) inner.style.transform = `translateX(${dx}px)` } }, { passive: true })
|
||||
area.addEventListener('touchend', (e) => {
|
||||
dragging = false
|
||||
const t = e.changedTouches[0]; const dx = t.clientX - x0; const dy = t.clientY - y0
|
||||
const inner = document.getElementById('cards-inner')
|
||||
const threshold = 40
|
||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > threshold) { if (dx < 0) animateRotate('next'); else animateRotate('prev'); pauseAutoplay(WALL_CONF.swipe_pause_ms) }
|
||||
else { if (inner) { inner.style.transition = 'transform 300ms ease'; inner.style.transform = 'translateX(0px)'; setTimeout(() => { inner.style.transition = ''; inner.style.transform = '' }, 300) } }
|
||||
})
|
||||
}
|
||||
|
||||
const animateRotate = (dir) => {
|
||||
const inner = document.getElementById('cards-inner')
|
||||
const box = elCards()
|
||||
if (!inner || !box) { if (dir === 'next') rotate(); else rotatePrev(); return }
|
||||
const step = Math.round(box.clientWidth / 2)
|
||||
inner.style.transition = 'transform 300ms ease'
|
||||
inner.style.transform = dir === 'next' ? `translateX(-${step}px)` : `translateX(${step}px)`
|
||||
setTimeout(() => { inner.style.transition = ''; inner.style.transform = ''; if (dir === 'next') rotate(); else rotatePrev() }, 300)
|
||||
}
|
||||
|
||||
const renderPreview = () => {
|
||||
const box = document.getElementById('preview'); if (!box) return
|
||||
box.innerHTML = ''
|
||||
for (let i = 0; i < SCENES.length; i++) {
|
||||
const c = SCENES[i]
|
||||
const t = document.createElement('div')
|
||||
t.className = 'thumb'
|
||||
t.setAttribute('data-idx', String(i))
|
||||
const title = document.createElement('div'); title.className = 'title'; title.textContent = c.title
|
||||
t.appendChild(title)
|
||||
t.addEventListener('click', () => { currentIndex = i; renderCards(); updatePreviewActive(); pauseAutoplay(WALL_CONF.swipe_pause_ms) })
|
||||
box.appendChild(t)
|
||||
}
|
||||
updatePreviewActive()
|
||||
fitLayout()
|
||||
}
|
||||
|
||||
const openSettings = async () => {
|
||||
const res = await fetch('/api/wall/config')
|
||||
const data = await res.json()
|
||||
if (data.ok) {
|
||||
const cfg = data.config || {}
|
||||
document.getElementById('autoplay-ms').value = cfg.autoplay_ms || WALL_CONF.autoplay_ms
|
||||
document.getElementById('swipe-pause-ms').value = cfg.swipe_pause_ms || WALL_CONF.swipe_pause_ms
|
||||
document.getElementById('max-cards').value = cfg.max_cards_on_screen || WALL_CONF.max_cards_on_screen
|
||||
const bg = cfg.background || {}
|
||||
document.getElementById('bg-url').value = bg.url || ''
|
||||
document.getElementById('bg-mode').value = bg.mode || 'tile'
|
||||
document.getElementById('bg-size').value = bg.size || 120
|
||||
document.getElementById('bg-opacity').value = bg.opacity || 0.08
|
||||
}
|
||||
document.getElementById('settings-modal').classList.remove('hidden')
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
const body = {
|
||||
autoplay_ms: parseInt(document.getElementById('autoplay-ms').value, 10) || WALL_CONF.autoplay_ms,
|
||||
swipe_pause_ms: parseInt(document.getElementById('swipe-pause-ms').value, 10) || WALL_CONF.swipe_pause_ms,
|
||||
max_cards_on_screen: parseInt(document.getElementById('max-cards').value, 10) || WALL_CONF.max_cards_on_screen,
|
||||
background: {
|
||||
url: String(document.getElementById('bg-url').value || ''),
|
||||
mode: String(document.getElementById('bg-mode').value || 'tile'),
|
||||
size: parseInt(document.getElementById('bg-size').value || '120', 10),
|
||||
opacity: Number(document.getElementById('bg-opacity').value || '0.08')
|
||||
}
|
||||
}
|
||||
await fetch('/api/wall/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
document.getElementById('settings-modal').classList.add('hidden')
|
||||
await initWall()
|
||||
}
|
||||
|
||||
const resetCardForm = () => {
|
||||
const title = document.getElementById('cards-edit-title'); if (title) title.value = ''
|
||||
const items = document.getElementById('cards-edit-items'); if (items) items.value = ''
|
||||
const meta = document.getElementById('cards-edit-meta'); if (meta) meta.value = ''
|
||||
}
|
||||
|
||||
const reloadCardsTable = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/wall/cards')
|
||||
const data = await res.json()
|
||||
if (!data.ok) return
|
||||
const tbody = document.getElementById('cards-table-body')
|
||||
if (!tbody) return
|
||||
tbody.innerHTML = ''
|
||||
const rows = Array.isArray(data.rows) ? data.rows : []
|
||||
rows.forEach(row => {
|
||||
const tr = document.createElement('tr')
|
||||
const tdTitle = document.createElement('td')
|
||||
tdTitle.textContent = row.title || ''
|
||||
const tdMeta = document.createElement('td')
|
||||
tdMeta.textContent = row.meta || ''
|
||||
const tdOps = document.createElement('td')
|
||||
const btnEdit = document.createElement('button')
|
||||
btnEdit.className = 'btn'
|
||||
btnEdit.textContent = '编辑'
|
||||
btnEdit.addEventListener('click', () => fillCardForm(row))
|
||||
const btnDel = document.createElement('button')
|
||||
btnDel.className = 'btn'
|
||||
btnDel.textContent = '删除'
|
||||
btnDel.style.marginLeft = '4px'
|
||||
btnDel.addEventListener('click', async () => {
|
||||
if (!confirm('确定删除该卡片?')) return
|
||||
await fetch(`/api/wall/cards/${row.id}`, { method: 'DELETE' })
|
||||
await reloadCardsTable()
|
||||
await initWall()
|
||||
})
|
||||
tdOps.appendChild(btnEdit)
|
||||
tdOps.appendChild(btnDel)
|
||||
tr.appendChild(tdTitle)
|
||||
tr.appendChild(tdMeta)
|
||||
tr.appendChild(tdOps)
|
||||
tbody.appendChild(tr)
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const openCardsMaintain = async () => {
|
||||
editingCardId = null
|
||||
resetCardForm()
|
||||
await reloadCardsTable()
|
||||
const m = document.getElementById('cards-modal')
|
||||
if (m) m.classList.remove('hidden')
|
||||
}
|
||||
|
||||
const closeCardsMaintain = () => {
|
||||
const m = document.getElementById('cards-modal')
|
||||
if (m) m.classList.add('hidden')
|
||||
initWall()
|
||||
}
|
||||
|
||||
const fillCardForm = (row) => {
|
||||
editingCardId = row.id
|
||||
const title = document.getElementById('cards-edit-title'); if (title) title.value = row.title || ''
|
||||
const items = document.getElementById('cards-edit-items'); if (items) items.value = (Array.isArray(row.items) ? row.items : []).join('\n')
|
||||
const meta = document.getElementById('cards-edit-meta'); if (meta) meta.value = row.meta || ''
|
||||
}
|
||||
|
||||
const saveCardFromForm = async () => {
|
||||
const titleEl = document.getElementById('cards-edit-title')
|
||||
const itemsEl = document.getElementById('cards-edit-items')
|
||||
const metaEl = document.getElementById('cards-edit-meta')
|
||||
if (!titleEl || !itemsEl || !metaEl) return
|
||||
const title = String(titleEl.value || '').trim()
|
||||
const meta = String(metaEl.value || '')
|
||||
const items = String(itemsEl.value || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
|
||||
if (!title) return
|
||||
const payload = { title, meta, items }
|
||||
if (editingCardId) {
|
||||
await fetch(`/api/wall/cards/${editingCardId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
||||
} else {
|
||||
await fetch('/api/wall/cards', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
||||
}
|
||||
await reloadCardsTable()
|
||||
await initWall()
|
||||
}
|
||||
|
||||
const initWall = async () => {
|
||||
let cfg = null
|
||||
let scenes = []
|
||||
try {
|
||||
const cfgResp = await fetch('/api/wall/config')
|
||||
const cardsResp = await fetch('/api/wall/cards').catch(() => null)
|
||||
const cfgData = await cfgResp.json()
|
||||
if (cfgData.ok) {
|
||||
cfg = cfgData.config || {}
|
||||
WALL_CONF = {
|
||||
autoplay_ms: cfg.autoplay_ms || WALL_CONF.autoplay_ms,
|
||||
swipe_pause_ms: cfg.swipe_pause_ms || WALL_CONF.swipe_pause_ms,
|
||||
max_cards_on_screen: cfg.max_cards_on_screen || WALL_CONF.max_cards_on_screen
|
||||
}
|
||||
}
|
||||
if (cardsResp) {
|
||||
const cardsData = await cardsResp.json()
|
||||
if (cardsData.ok && Array.isArray(cardsData.rows)) {
|
||||
scenes = cardsData.rows.map(r => ({
|
||||
title: String(r.title || ''),
|
||||
items: Array.isArray(r.items) ? r.items.map(x => String(x || '')) : [],
|
||||
meta: String(r.meta || '')
|
||||
}))
|
||||
}
|
||||
}
|
||||
if ((!scenes || scenes.length === 0) && cfg && Array.isArray(cfg.cards)) {
|
||||
scenes = cfg.cards.map(c => ({
|
||||
title: String(c.title || ''),
|
||||
items: Array.isArray(c.items) ? c.items.map(x => String(x || '')) : [],
|
||||
meta: String(c.meta || '')
|
||||
}))
|
||||
}
|
||||
} catch {}
|
||||
SCENES = Array.isArray(scenes) ? scenes : []
|
||||
if (SCENES.length === 0) SCENES = [{ title: '示例卡片', items: ['在设置中添加你的提醒'], meta: '示例' }]
|
||||
currentIndex = 0
|
||||
renderCards()
|
||||
renderPreview()
|
||||
startAutoplay()
|
||||
bindSwipe()
|
||||
loadLogs()
|
||||
fitLayout()
|
||||
applyScrollPolicy()
|
||||
applyCardsBackground()
|
||||
}
|
||||
|
||||
const fitLayout = () => {
|
||||
const wall = document.getElementById('wall')
|
||||
const cards = elCards()
|
||||
const preview = document.getElementById('preview')
|
||||
const controls = document.querySelector('.controls')
|
||||
if (!wall || !cards) return
|
||||
const avail = wall.clientHeight - ((preview && preview.offsetHeight) || 0) - ((controls && controls.offsetHeight) || 0) - 16
|
||||
const needed = cards.scrollHeight
|
||||
let scale = 1
|
||||
if (avail > 0 && needed > avail) {
|
||||
scale = Math.max(0.7, Math.min(1, avail / needed))
|
||||
}
|
||||
const isPhone = window.innerWidth <= 480
|
||||
const baseTitle = isPhone ? 22 : 32
|
||||
const baseList = isPhone ? 16 : 24
|
||||
const basePad = isPhone ? 12 : 24
|
||||
const baseWeatherPad = isPhone ? 8 : 12
|
||||
const set = (k, v) => document.documentElement.style.setProperty(k, `${Math.round(v)}px`)
|
||||
set('--card-title-size', baseTitle * scale)
|
||||
set('--card-list-size', baseList * scale)
|
||||
set('--card-padding', basePad * scale)
|
||||
set('--weather-padding', baseWeatherPad * scale)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initWall()
|
||||
const bs = document.getElementById('btn-settings'); if (bs) bs.addEventListener('click', openSettings)
|
||||
const bl = document.getElementById('btn-prev-card'); if (bl) bl.addEventListener('click', () => { animateRotate('prev'); pauseAutoplay(WALL_CONF.swipe_pause_ms) })
|
||||
const br = document.getElementById('btn-next-card'); if (br) br.addEventListener('click', () => { animateRotate('next'); pauseAutoplay(WALL_CONF.swipe_pause_ms) })
|
||||
const bsv = document.getElementById('btn-save'); if (bsv) bsv.addEventListener('click', saveSettings)
|
||||
const bc = document.getElementById('btn-close'); if (bc) bc.addEventListener('click', () => document.getElementById('settings-modal').classList.add('hidden'))
|
||||
const bm = document.getElementById('btn-cards-maintain'); if (bm) bm.addEventListener('click', openCardsMaintain)
|
||||
const bAdd = document.getElementById('cards-btn-add'); if (bAdd) bAdd.addEventListener('click', () => { editingCardId = null; resetCardForm() })
|
||||
const bCardSave = document.getElementById('cards-btn-save'); if (bCardSave) bCardSave.addEventListener('click', () => { saveCardFromForm() })
|
||||
const bCardCancel = document.getElementById('cards-btn-cancel'); if (bCardCancel) bCardCancel.addEventListener('click', () => { closeCardsMaintain() })
|
||||
window.wallOpenSettings = openSettings
|
||||
const bcp = document.getElementById('w-btn-copy'); if (bcp) bcp.addEventListener('click', async () => { try { await navigator.clipboard.writeText((wOut() && wOut().textContent) || '') } catch {} })
|
||||
const brf = document.getElementById('w-btn-refresh'); if (brf) brf.addEventListener('click', loadLogs)
|
||||
const bcl = document.getElementById('w-btn-clear'); if (bcl) bcl.addEventListener('click', async () => { try { await fetch('/api/debug/clear?channel=wall', { method: 'POST' }) } catch {}; loadLogs() })
|
||||
window.addEventListener('resize', () => fitLayout())
|
||||
window.addEventListener('resize', () => applyScrollPolicy())
|
||||
window.addEventListener('orientationchange', () => applyScrollPolicy())
|
||||
window.addEventListener('resize', () => applyCardsBackground())
|
||||
})
|
||||
const wOut = () => document.getElementById('w-debug-output')
|
||||
const mts = () => (window.fmtCNDateTime ? window.fmtCNDateTime(new Date()) : new Date().toLocaleString('zh-CN'))
|
||||
const logLocal = msg => { const out = wOut(); if (!out) return; const line = `${mts()} ${msg}`; const c = out.textContent || ''; out.textContent = c ? c + "\n" + line : line; out.scrollTop = out.scrollHeight }
|
||||
const loadLogs = async () => { try { const res = await fetch('/api/debug/logs?limit=5000&channel=wall'); const data = await res.json(); const out = wOut(); if (data.ok && out) { out.textContent = (data.lines || []).join('\n'); out.scrollTop = out.scrollHeight } } catch {}
|
||||
}
|
||||
|
||||
const applyScrollPolicy = () => {
|
||||
const ua = navigator.userAgent || ''
|
||||
const isPhoneUA = /iPhone|Android.+Mobile/i.test(ua)
|
||||
const portraitSmall = window.innerWidth <= 480 && window.matchMedia('(orientation: portrait)').matches
|
||||
const allowScroll = isPhoneUA || portraitSmall
|
||||
const html = document.documentElement
|
||||
const body = document.body
|
||||
if (allowScroll) { html.style.overflowY = 'auto'; html.style.overflowX = 'hidden'; body.style.overflowY = 'auto'; body.style.overflowX = 'hidden' }
|
||||
else { html.style.overflow = 'hidden'; body.style.overflow = 'hidden' }
|
||||
}
|
||||
|
||||
const applyCardsBackground = async () => {
|
||||
try {
|
||||
const [cfgResp, listResp] = await Promise.all([
|
||||
fetch('/api/wall/config'),
|
||||
fetch('/api/wall/backgrounds')
|
||||
])
|
||||
const data = await cfgResp.json()
|
||||
const listData = await listResp.json().catch(() => ({ ok:false, images: [] }))
|
||||
const cfg = (data && data.config) || {}
|
||||
const bg = cfg.background || {}
|
||||
let url = String(bg.url || '')
|
||||
if (!url) {
|
||||
const arr = Array.isArray(listData.images) ? listData.images : []
|
||||
if (arr.length > 0) url = arr[Math.floor(Math.random() * arr.length)]
|
||||
else url = '/tools/wall/bjt.jpg'
|
||||
}
|
||||
const mode = String(bg.mode || 'tile')
|
||||
const isPhone = window.innerWidth <= 480
|
||||
const size = parseInt(bg.size || (isPhone ? 72 : 120), 10)
|
||||
const opacity = Math.max(0, Math.min(1, Number(bg.opacity || 0.12)))
|
||||
// apply to CSS variables for card pseudo background
|
||||
const root = document.documentElement.style
|
||||
root.setProperty('--card-bg-image', `url(${url})`)
|
||||
root.setProperty('--card-bg-repeat', mode === 'cover' ? 'no-repeat' : 'repeat')
|
||||
root.setProperty('--card-bg-size', mode === 'cover' ? 'cover' : `${size}px ${size}px`)
|
||||
root.setProperty('--card-bg-opacity', String(opacity))
|
||||
// remove legacy overlay if exists
|
||||
const ov = document.getElementById('cards-bg')
|
||||
if (ov && ov.parentNode) ov.parentNode.removeChild(ov)
|
||||
} catch {}
|
||||
}
|
||||
Reference in New Issue
Block a user