chore: 初始提交 - 移除所有硬编码凭据,统一从环境变量读取
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"_comment": "墙(Wall)工具的前端鉴权配置",
|
||||
"enable_auth_comment": "true:启用鉴权(需要从导航登录);false:完全放开访问",
|
||||
"enable_auth": true,
|
||||
"max_age_days_comment": "鉴权 cookie 的最大有效天数(1~365)",
|
||||
"max_age_days": 30
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Yang's Toolbox · 告知墙</title>
|
||||
<link rel="icon" href="/icons/wall.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/common/common.css" />
|
||||
<link rel="stylesheet" href="/tools/wall/wall.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section class="debug" id="w-debug-section" style="display:none">
|
||||
<div class="debug-actions">
|
||||
<button id="w-btn-copy" class="btn">复制调试信息</button>
|
||||
<button id="w-btn-refresh" class="btn">刷新</button>
|
||||
<button id="w-btn-clear" class="btn">清空</button>
|
||||
</div>
|
||||
<div id="w-debug-output" class="debug-output"></div>
|
||||
</section>
|
||||
<section id="wall" class="wall" aria-label="提醒墙">
|
||||
<div id="cards" class="cards" tabindex="0"></div>
|
||||
<div id="preview" class="preview"></div>
|
||||
<div class="controls">
|
||||
<button id="btn-prev-card" class="btn" title="上一屏">⬅️</button>
|
||||
<button id="btn-next-card" class="btn" title="下一屏">➡️</button>
|
||||
<button id="btn-settings" class="btn" onclick="window.wallOpenSettings ? window.wallOpenSettings() : (function(){var m=document.getElementById('settings-modal'); if(m) m.classList.remove('hidden')})()">设置</button>
|
||||
<button id="btn-cards-maintain" class="btn">数据维护</button>
|
||||
<div id="status" class="status" style="display:none"></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="modal hidden" id="settings-modal">
|
||||
<div class="modal-body wall-settings-modal">
|
||||
<h3>告知墙设置</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:8px">
|
||||
<div><label>自动播放(ms)</label><input id="autoplay-ms" type="number" value="45000" /></div>
|
||||
<div><label>滑动暂停(ms)</label><input id="swipe-pause-ms" type="number" value="300000" /></div>
|
||||
<div><label>同时展示卡片数</label><input id="max-cards" type="number" value="2" min="1" max="3" /></div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:8px">
|
||||
<div style="grid-column:1 / span 2"><label>背景图URL</label><input id="bg-url" type="text" placeholder="https://.../bg.png" /></div>
|
||||
<div><label>模式</label><select id="bg-mode"><option value="tile">平铺</option><option value="cover">铺满</option></select></div>
|
||||
<div><label>平铺尺寸(px)</label><input id="bg-size" type="number" value="120" /></div>
|
||||
<div><label>透明度(0-1)</label><input id="bg-opacity" type="number" step="0.01" min="0" max="1" value="0.08" /></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="btn-save" class="btn">保存</button>
|
||||
<button id="btn-close" class="btn">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal hidden" id="cards-modal">
|
||||
<div class="modal-body wall-cards-modal">
|
||||
<h3>公告数据维护</h3>
|
||||
<div class="cards-modal-toolbar">
|
||||
<div>
|
||||
<button id="cards-btn-add" class="btn">新增卡片</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards-modal-main">
|
||||
<div class="cards-modal-list">
|
||||
<table class="table cards-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left">标题</th>
|
||||
<th style="text-align:left">备注</th>
|
||||
<th style="text-align:left">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cards-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="cards-modal-editor">
|
||||
<div>
|
||||
<label>标题</label>
|
||||
<input id="cards-edit-title" type="text" />
|
||||
</div>
|
||||
<div class="cards-modal-textarea">
|
||||
<label>内容(每行一条)</label>
|
||||
<textarea id="cards-edit-items" rows="6"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>备注</label>
|
||||
<input id="cards-edit-meta" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="cards-btn-save" class="btn">保存</button>
|
||||
<button id="cards-btn-cancel" class="btn">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<script src="/common/common.js"></script>
|
||||
<script src="/tools/wall/wall.js"></script>
|
||||
<script>
|
||||
(async function(){
|
||||
try {
|
||||
const res = await fetch('/api/flags')
|
||||
const data = await res.json()
|
||||
if (data && data.debug && data.debug.wall) {
|
||||
const sec = document.getElementById('w-debug-section')
|
||||
if (sec) sec.style.display = ''
|
||||
}
|
||||
} catch {}
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,205 @@
|
||||
.root, :root { --card-title-size: 32px; --card-list-size: 24px; --card-padding: 24px }
|
||||
.html, body { height: 100%; overflow: hidden }
|
||||
.wall { display: grid; gap: 12px; padding: 8px; min-height: calc(100vh - 60px); height: 100vh; box-sizing: border-box }
|
||||
.cards { display: grid; grid-template-columns: 1fr; grid-auto-rows: 1fr; height: 100%; gap: 0; align-content: stretch; overflow: hidden }
|
||||
.cards-inner { display: flex; height: 100%; gap: 12px; will-change: transform; transition: transform 300ms ease }
|
||||
.cards-inner .card { flex: 0 0 calc(50% - 6px) }
|
||||
.cards { position: relative }
|
||||
.card { border: 2px solid #111; border-radius: 16px; padding: var(--card-padding); background: rgba(255,255,255,0.96) }
|
||||
.card { height: 100%; display: flex; flex-direction: column; position: relative; z-index: 1; overflow: hidden }
|
||||
.card::before { content: ""; position: absolute; inset: 0; border-radius: 16px; background-image: var(--card-bg-image, none); background-repeat: var(--card-bg-repeat, repeat); background-size: var(--card-bg-size, 120px 120px); background-position: center; opacity: var(--card-bg-opacity, 0.08); z-index: 0 }
|
||||
.card > * { position: relative; z-index: 1 }
|
||||
.card .list { flex: 1; font-size: var(--card-list-size) }
|
||||
.card .lines { flex: 1; font-size: var(--card-list-size); line-height: 1.5 }
|
||||
.card .lines div { margin: 6px 0 }
|
||||
.card .title { font-size: var(--card-title-size) }
|
||||
.card .title { font-size: 32px; font-weight: 700; margin-bottom: 12px }
|
||||
.card .list { font-size: 24px; line-height: 1.5 }
|
||||
.card .list li { margin: 6px 0 }
|
||||
.card .meta { margin-top: 8px; font-size: 16px; color: #666 }
|
||||
.status { margin: 8px 0; padding: 8px; border: 1px solid #ffd666; background: #fffbe6; color: #a87000; border-radius: 8px }
|
||||
.controls { display: flex; gap: 8px; align-items: center }
|
||||
.preview { display: grid; grid-template-columns: repeat(auto-fill, minmax(105px, 1fr)); gap: 6px; max-height: 100%; overflow-y: auto }
|
||||
.thumb { border: 1px solid #bbb; border-radius: 10px; padding: 6px; background: #fff; cursor: pointer }
|
||||
.thumb .title { font-size: 12px; font-weight: 600; margin-bottom: 4px }
|
||||
.preview .title { display: block; max-width: 6em; word-break: break-all; overflow-wrap: anywhere; white-space: normal }
|
||||
.thumb .list { display: none }
|
||||
.thumb.active { border-color: #1677ff }
|
||||
.debug { margin: 12px 0; padding: 8px; border: 1px dashed #aaa; }
|
||||
.debug-actions { display: flex; gap: 8px; margin-bottom: 8px }
|
||||
.debug-output { height: 240px; overflow: auto; background: #fafafa; padding: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; white-space: pre-wrap }
|
||||
|
||||
/* Landscape preference */
|
||||
@media (orientation: landscape) {
|
||||
.wall {
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: 1fr auto;
|
||||
grid-template-areas:
|
||||
"cards preview"
|
||||
"controls controls";
|
||||
align-items: start;
|
||||
}
|
||||
.preview { display: grid; grid-template-columns: 1fr; grid-auto-rows: min-content; gap: 6px; overflow: auto; width: 8em; box-sizing: border-box }
|
||||
.thumb { min-width: unset; width: 100% }
|
||||
.preview .title { max-width: 8em }
|
||||
.weather { grid-column: 1 / -1 !important }
|
||||
.controls { grid-column: 1 / -1 !important; width: 100% }
|
||||
}
|
||||
|
||||
/* Portrait phones (iPhone 14/Pro/Max widths ~393–430px) */
|
||||
@media (max-width: 480px) {
|
||||
.wall { gap: 8px; padding: 6px; min-height: 100vh }
|
||||
.cards { gap: 8px }
|
||||
.card { padding: var(--card-padding) }
|
||||
.card .title { font-size: var(--card-title-size) }
|
||||
.card .list { font-size: var(--card-list-size) }
|
||||
.preview { display: flex; gap: 6px; overflow: hidden; flex-wrap: nowrap }
|
||||
.thumb { min-width: 90px; padding: 4px }
|
||||
.thumb .title { font-size: 11px }
|
||||
.thumb .list { font-size: 9px; line-height: 1.25 }
|
||||
.controls { gap: 6px }
|
||||
}
|
||||
|
||||
/* Portrait mobile: allow vertical scroll */
|
||||
@media (max-width: 480px) and (orientation: portrait) {
|
||||
html, body { overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch }
|
||||
.wall { min-height: auto }
|
||||
}
|
||||
|
||||
/* Mobile iPad/iPhone friendly */
|
||||
@media (max-width: 800px) {
|
||||
.card .title { font-size: 28px }
|
||||
.card .list { font-size: 20px }
|
||||
.debug-output { height: 160px }
|
||||
.controls { flex-wrap: wrap }
|
||||
}
|
||||
/* Portrait strict sizing for preview and weather */
|
||||
@media (orientation: portrait) {
|
||||
.preview { height: 4em; display: flex; gap: 6px; overflow-x: auto; overflow-y: hidden; align-items: center }
|
||||
.thumb { height: 3.6em; padding: 4px 6px; display: flex; align-items: center }
|
||||
.preview .title { line-height: 1em; max-width: 8em; max-height: 4em; word-break: break-all; overflow: hidden; white-space: normal }
|
||||
}
|
||||
|
||||
.wall-settings-modal {
|
||||
max-width: 640px;
|
||||
width: 90vw;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wall-settings-modal h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.wall-settings-modal label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.wall-settings-modal input,
|
||||
.wall-settings-modal select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wall-cards-modal {
|
||||
width: 80vw;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
height: 80vh;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.22);
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wall-cards-modal h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.cards-modal-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cards-modal-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.4fr);
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cards-modal-list {
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #fafafa;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cards-modal-editor {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cards-modal-textarea {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cards-modal-textarea textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.cards-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cards-table th,
|
||||
.cards-table td {
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.cards-table tr:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.wall-cards-modal label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.wall-cards-modal input,
|
||||
.wall-cards-modal textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.wall-cards-modal {
|
||||
width: 96vw;
|
||||
max-width: 96vw;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
}
|
||||
.cards-modal-main {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
||||
}
|
||||
}
|
||||
@@ -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