feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>周报查看</title>
<link rel="icon" href="/icons/weekly.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/common/common.css" />
<link rel="stylesheet" href="/tools/weekly_embed/weekly_embed.css" />
</head>
<body class="theme-cloud" data-page="report">
<div class="page">
<header class="top-bar">
<div class="time-controls">
<button id="btn-prev-week" class="btn btn-secondary">上一周</button>
<select id="week-year"></select>
<select id="week-list"></select>
<button id="btn-next-week" class="btn btn-secondary">下一周</button>
</div>
<div class="nav-links">
<a id="link-report" class="nav-link active" href="index.html">周报</a>
<a id="link-events" class="nav-link" href="events.html">事件列表</a>
</div>
</header>
<main class="content">
<section class="card">
<div class="card-header">
<h2>周报标题</h2>
<button id="btn-refresh-report" class="btn">刷新</button>
</div>
<div id="report-title" class="report-title muted">暂无周报</div>
</section>
<section class="card">
<h3>周报摘要</h3>
<div id="report-summary" class="report-summary muted">暂无摘要</div>
</section>
<section class="card">
<h3>周报内容</h3>
<div id="report-content" class="report-content muted">暂无内容</div>
</section>
<div id="status" class="status" role="status" aria-live="polite"></div>
</main>
</div>
<div id="loading-overlay" role="dialog" aria-modal="true" aria-labelledby="loading-title">
<div id="loading-panel" tabindex="-1">
<div id="loading-header">
<div id="loading-title">加载中</div>
<button id="loading-close" aria-label="关闭">×</button>
</div>
<div id="loading-body">
<div id="loading-logs"></div>
</div>
<div id="loading-footer">
<button id="btn-copy-logs" class="btn btn-secondary" disabled>复制日志</button>
<button id="btn-loading-close" class="btn btn-secondary" disabled>关闭</button>
</div>
</div>
</div>
<script src="/tools/weekly_embed/weekly_embed.js"></script>
</body>
</html>
+321
View File
@@ -0,0 +1,321 @@
:root {
--primary: #1e2a3a;
--primary-light: #2c3e50;
--accent: #2d8cff;
--accent-hover: #1a75d9;
--bg: #f6f7fb;
--surface: #ffffff;
--border: #e5e7eb;
--text: #1f2937;
--text-muted: #6b7280;
--radius: 6px;
--radius-sm: 4px;
--shadow: rgba(0, 0, 0, 0.06);
--transition: all 0.2s ease;
}
body {
margin: 0;
color: var(--text);
background: var(--bg);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.page {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 14px 20px;
background: var(--primary);
color: #fff;
position: sticky;
top: 0;
z-index: 10;
}
.time-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.nav-links {
display: flex;
gap: 10px;
align-items: center;
}
.nav-link {
color: #e5e7eb;
text-decoration: none;
padding: 6px 12px;
border-radius: var(--radius-sm);
transition: var(--transition);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.nav-link.active,
.nav-link:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.btn:focus-visible,
.nav-link:focus-visible,
select:focus-visible,
#loading-close:focus-visible,
#btn-loading-close:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.content {
flex: 1;
padding: 18px;
max-width: 1200px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 1px 3px var(--shadow);
padding: 18px;
margin-bottom: 16px;
}
.card h2,
.card h3 {
margin: 0 0 12px 0;
color: var(--primary);
font-weight: 600;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
select {
padding: 6px 10px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: var(--radius-sm);
background: #fff;
color: var(--primary);
}
.btn {
padding: 6px 12px;
border: none;
border-radius: var(--radius-sm);
background: var(--accent);
color: #fff;
cursor: pointer;
transition: var(--transition);
}
.btn:hover {
background: var(--accent-hover);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.25);
}
.report-title {
font-size: 18px;
font-weight: 600;
}
.report-summary,
.report-content {
line-height: 1.7;
color: var(--text);
font-size: 14px;
word-break: break-word;
}
.report-content {
min-height: 180px;
}
.report-content img,
.event-body img {
max-width: 100%;
height: auto;
}
.report-content pre,
.event-body pre {
overflow: auto;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: rgba(0, 0, 0, 0.03);
}
.events {
display: grid;
gap: 14px;
}
.event {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px;
background: #fff;
}
.event-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.tag {
padding: 2px 8px;
border-radius: 999px;
background: rgba(45, 140, 255, 0.1);
color: var(--accent);
font-size: 12px;
}
.event-title {
font-weight: 600;
color: var(--primary);
}
.event-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 8px;
}
.event-body {
font-size: 14px;
line-height: 1.6;
color: var(--text);
}
.status {
margin-top: 10px;
color: #ef4444;
font-size: 13px;
}
.muted {
color: var(--text-muted);
}
.empty {
color: var(--text-muted);
text-align: center;
padding: 20px 0;
}
#loading-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.35);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
}
#loading-panel {
width: 680px;
max-width: calc(100vw - 40px);
max-height: calc(100vh - 80px);
background: #fff;
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
}
#loading-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
#loading-title {
font-weight: 600;
color: var(--primary);
}
#loading-close {
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
}
#loading-body {
padding: 10px 16px;
overflow: auto;
}
#loading-logs {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
}
#loading-footer {
padding: 10px 16px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
.log-step {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 6px 0;
}
.log-step .icon {
width: 18px;
}
.log-step .text {
flex: 1;
}
@media (max-width: 900px) {
.top-bar {
flex-direction: column;
align-items: flex-start;
}
.content {
padding: 14px;
}
}
+490
View File
@@ -0,0 +1,490 @@
;(function () {
const el = id => document.getElementById(id)
const $year = el('week-year')
const $weeks = el('week-list')
const $status = el('status')
const $title = el('report-title')
const $summary = el('report-summary')
const $content = el('report-content')
const $events = el('events')
const $prev = el('btn-prev-week')
const $next = el('btn-next-week')
const $refreshReport = el('btn-refresh-report')
const $refreshEvents = el('btn-refresh-events')
const page = document.body.getAttribute('data-page') || 'report'
const $overlay = el('loading-overlay')
const $logs = el('loading-logs')
const $ltitle = el('loading-title')
const $btnClose = el('btn-loading-close')
const $btnCopy = el('btn-copy-logs')
const $btnX = el('loading-close')
const qs = new URLSearchParams(location.search)
const urlKey = qs.get('key') || ''
const storedKey = (() => { try { return sessionStorage.getItem('weekly_embed_key') || '' } catch { return '' } })()
const accessKey = urlKey || storedKey || ''
const apiBase = '/api/weekly_embed'
const persistKey = () => {
if (!urlKey || !accessKey) return
try { sessionStorage.setItem('weekly_embed_key', accessKey) } catch {}
try {
const u = new URL(location.href)
u.searchParams.delete('key')
history.replaceState(null, '', `${u.pathname}${u.search}${u.hash}`)
} catch {}
}
const openLoading = (title) => {
if ($overlay) {
$overlay.style.display = 'flex'
if ($logs) $logs.innerHTML = ''
if ($ltitle) $ltitle.textContent = title || '加载中'
if ($btnClose) { $btnClose.disabled = true; $btnClose.onclick = () => { $overlay.style.display = 'none' } }
if ($btnCopy) { $btnCopy.disabled = true }
if ($btnX) { $btnX.disabled = true; $btnX.onclick = () => { if (!$btnClose || !$btnClose.disabled) $overlay.style.display = 'none' } }
}
}
const maskKeyInText = (text) => {
if (!accessKey) return String(text || '')
return String(text || '').split(accessKey).join('***')
}
const getLogsText = () => {
const title = ($ltitle && $ltitle.textContent) ? $ltitle.textContent : '加载中'
const body = $logs ? $logs.innerText : ''
return `${title}\n${body}`.trim()
}
const copyLogs = async () => {
const text = getLogsText()
if (!text) return
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text)
} else {
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'
ta.style.left = '-9999px'
ta.style.top = '0'
document.body.appendChild(ta)
ta.focus()
ta.select()
document.execCommand('copy')
ta.remove()
}
addLoadingStep('已复制到剪贴板', '📋')
} catch (e) {
addLoadingStep(`复制失败:${String(e && e.message ? e.message : e)}`, '⚠️')
}
}
const addLoadingStep = (text, icon = '⏳') => {
if (!$logs) return
const row = document.createElement('div')
row.className = 'log-step'
const i = document.createElement('div'); i.className = 'icon'; i.textContent = icon
const t = document.createElement('div'); t.className = 'text'; t.textContent = maskKeyInText(text)
row.appendChild(i); row.appendChild(t)
$logs.appendChild(row)
$logs.scrollTop = $logs.scrollHeight
if ($btnCopy) $btnCopy.disabled = false
}
const closeLoading = (ok) => {
if ($overlay) {
if ($btnClose) $btnClose.disabled = false
if ($btnCopy) $btnCopy.disabled = !$logs || !$logs.innerText
if ($btnX) $btnX.disabled = false
if (ok) setTimeout(() => { $overlay.style.display = 'none' }, 200)
}
}
const runWithLoading = async (fn, title) => {
openLoading(title || '加载中')
try {
await fn()
addLoadingStep('完成', '✔️')
closeLoading(true)
} catch (e) {
if ($ltitle) $ltitle.textContent = '发生错误'
addLoadingStep(String(e && (e.stack || e.message) ? (e.stack || e.message) : (e || '失败')), '❌')
closeLoading(false)
throw e
}
}
const setStatus = (msg, type) => {
if (!$status) return
$status.textContent = msg || ''
$status.style.color = type === 'success' ? '#10b981' : '#ef4444'
}
const setNavLinks = () => {
const linkReport = el('link-report')
const linkEvents = el('link-events')
const reportPage = location.pathname.endsWith('weekly_embed.html') ? 'weekly_embed.html' : 'index.html'
if (linkReport) linkReport.href = `${reportPage}`
if (linkEvents) linkEvents.href = `events.html`
}
const sanitizeHtml = (html) => {
const tpl = document.createElement('template')
tpl.innerHTML = String(html || '')
const blocked = new Set(['SCRIPT', 'IFRAME', 'OBJECT', 'EMBED', 'LINK', 'META', 'STYLE', 'BASE'])
const safeUrl = (raw) => {
const s = String(raw || '').trim()
if (!s) return ''
if (s.startsWith('#') || s.startsWith('/') || s.startsWith('./') || s.startsWith('../')) return s
if (/^(https?:|mailto:|tel:)/i.test(s)) return s
if (/^data:image\//i.test(s)) return s
return ''
}
const walk = (root) => {
const nodes = Array.from(root.querySelectorAll('*'))
nodes.forEach(node => {
if (blocked.has(node.tagName)) { node.remove(); return }
Array.from(node.attributes).forEach(attr => {
const name = attr.name.toLowerCase()
const val = attr.value
if (name.startsWith('on')) { node.removeAttribute(attr.name); return }
if (name === 'srcdoc') { node.removeAttribute(attr.name); return }
if (name === 'href' || name === 'src' || name === 'xlink:href') {
const v = safeUrl(val)
if (!v) node.removeAttribute(attr.name)
else node.setAttribute(attr.name, v)
return
}
if (name === 'style') {
const s = String(val || '')
if (/expression\s*\(|javascript:/i.test(s)) node.removeAttribute(attr.name)
return
}
})
if (node.tagName === 'A') {
const target = (node.getAttribute('target') || '').toLowerCase()
if (target === '_blank') {
const rel = (node.getAttribute('rel') || '').split(/\s+/).filter(Boolean)
;['noopener', 'noreferrer'].forEach(v => { if (!rel.includes(v)) rel.push(v) })
node.setAttribute('rel', rel.join(' '))
}
}
})
}
walk(tpl.content)
return tpl.innerHTML
}
const fetchJson = async (path, params = {}) => {
const url = new URL(`${apiBase}${path}`, location.origin)
if (accessKey) url.searchParams.set('key', accessKey)
Object.entries({ ...params }).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v))
})
addLoadingStep(`请求 ${url.pathname}${url.search.replace(/key=[^&]+/i, 'key=***')}`, '🌐')
const res = await fetch(url.toString(), {
cache: 'no-store',
credentials: 'same-origin',
headers: { 'X-Weekly-Embed-Key': accessKey }
})
const raw = await res.text().catch(() => '')
let data = {}
try { data = raw ? JSON.parse(raw) : {} } catch { data = {} }
if (!res.ok) {
const reason = String((data && data.error) ? data.error : (res.statusText || '请求失败'))
addLoadingStep(`HTTP ${res.status} ${reason}`, '🧯')
if (raw && raw !== '[object Object]') addLoadingStep(`响应:${raw.slice(0, 2000)}`, '📦')
addLoadingStep(`key_len=${accessKey ? String(accessKey).length : 0}`, '🔑')
throw new Error(reason)
}
if (Array.isArray(data.trace)) data.trace.forEach(s => addLoadingStep(`后端:${s}`, '⚙️'))
return data
}
const formatCNDeadline = (ts) => {
if (!ts) return ''
const s = String(ts).replace('T', ' ')
return s.length >= 19 ? s.slice(0, 19) : (s.length >= 16 ? s.slice(0, 16) : s)
}
const statusLabel = (code) => {
return ({ doing: '▶️进行中', done: '✔️完成', pause: '⏸️暂停', new: '📅本周新增', close: '❌关闭', not: '👋未做', ok: '👌成功', fail: '⛔失败' })[code] || code || '未知'
}
let weekList = []
let currentYear = new Date().getFullYear()
let currentWeekId = ''
const updateWeekButtons = () => {
const idx = weekList.findIndex(w => w.week_id === $weeks.value)
if ($prev) $prev.disabled = idx < 0 || idx >= weekList.length - 1
if ($next) $next.disabled = idx <= 0
}
const toTime = (v) => {
const s = String(v || '')
if (!s) return 0
const t = Date.parse(s)
if (!Number.isNaN(t)) return t
return Date.parse(s.replace(/\//g, '-')) || 0
}
const renderWeekOptions = (list) => {
weekList = list.slice().sort((a, b) => {
const ta = toTime(a.end || a.start)
const tb = toTime(b.end || b.start)
if (ta !== tb) return tb - ta
return String(b.week_id || '').localeCompare(String(a.week_id || ''))
})
$weeks.innerHTML = ''
weekList.forEach(w => {
const opt = document.createElement('option')
opt.value = w.week_id
const type = w.week_id === currentWeekId ? '📗 ' : '📘 '
const icon = (w.has_report && w.has_events) ? ' 📅💬' : (w.has_report ? ' 📅' : (w.has_events ? ' 💬' : ''))
opt.textContent = `${type}${w.week_id} (${w.start}~${w.end})${icon}`
$weeks.appendChild(opt)
})
if (currentWeekId && weekList.find(w => w.week_id === currentWeekId)) $weeks.value = currentWeekId
else if (weekList.length) $weeks.value = weekList[0].week_id
updateWeekButtons()
}
const loadWeeks = async (year) => {
addLoadingStep(`准备计算 ${year} 年的 ISO 周`, '🗓️')
const data = await fetchJson('/weeks', { year })
currentWeekId = data.currentWeekId || ''
addLoadingStep('渲染周下拉', '🧩')
renderWeekOptions(data.weeks || [])
await loadCurrent()
}
const loadYears = async () => {
let years = []
try {
const data = await fetchJson('/years')
years = Array.isArray(data.years) ? data.years : []
} catch {}
if (!years.length) years = [currentYear, currentYear - 1]
years = Array.from(new Set(years.concat([currentYear, currentYear + 1, currentYear - 1]))).sort((a, b) => b - a)
$year.innerHTML = ''
years.forEach(y => {
const opt = document.createElement('option')
opt.value = y
opt.textContent = `${y} 年`
$year.appendChild(opt)
})
if (years.includes(currentYear)) $year.value = currentYear
else if (years.length) { currentYear = years[0]; $year.value = currentYear }
await loadWeeks(currentYear)
}
const setText = (node, text, placeholder) => {
if (!node) return
const value = String(text || '').trim()
if (!value) {
node.textContent = placeholder
node.classList.add('muted')
return
}
node.classList.remove('muted')
node.textContent = value
}
const setHtml = (node, html, placeholder) => {
if (!node) return
const value = String(html || '').trim()
if (!value) {
node.textContent = placeholder
node.classList.add('muted')
return
}
node.classList.remove('muted')
node.innerHTML = sanitizeHtml(value)
}
const TITLE_INLINE_HAN_LIMIT = 50
const normalizeInlineText = text => String(text || '').replace(/\s+/g, ' ').trim()
const normalizeBodyForTitle = content => normalizeInlineText(String(content || '').replace(/<br\s*\/?>/gi, ';').replace(/<[^>]+>/g, ' ').replace(/\r?\n+/g, ';'))
const countHanChars = text => (String(text || '').match(/[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/g) || []).length
const countDisplayChars = text => Array.from(String(text || '')).length
const takePrefixByHanLimit = (text, hanLimit) => {
const chars = Array.from(String(text || ''))
let han = 0
let out = ''
for (const ch of chars) {
const isHan = /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/.test(ch)
if (isHan) {
if (han >= hanLimit) break
han += 1
}
out += ch
}
return out.trim()
}
const hasTitlePolicyApplied = (title, detail) => {
const t = normalizeInlineText(title)
const d = normalizeInlineText(detail)
if (!d) return true
if (t.includes('【剩余') && t.includes('详见正文】')) return true
if (d.length >= 10 && t.includes(d.slice(0, 10))) return true
return false
}
const buildEventTitleByPolicy = (title, content) => {
const base = normalizeInlineText(title)
const detail = normalizeBodyForTitle(content)
if (!detail || hasTitlePolicyApplied(base, detail)) return base
const hanCount = countHanChars(detail)
const effectiveCount = hanCount > 0 ? hanCount : countDisplayChars(detail)
if (effectiveCount <= TITLE_INLINE_HAN_LIMIT) return normalizeInlineText(`${base} ${detail}`)
const prefix = hanCount > 0 ? takePrefixByHanLimit(detail, TITLE_INLINE_HAN_LIMIT) : Array.from(detail).slice(0, TITLE_INLINE_HAN_LIMIT).join('')
const remaining = Math.max(0, effectiveCount - TITLE_INLINE_HAN_LIMIT)
return normalizeInlineText(`${base} ${prefix}【剩余${remaining}汉字,详见正文】`)
}
const loadReport = async () => {
if (!$weeks.value) return
addLoadingStep(`获取周报 ${$weeks.value}`, '📄')
const data = await fetchJson('/report', { week_id: $weeks.value })
const report = data.report || null
addLoadingStep('更新标题/摘要/正文', '📝')
setText($title, report ? report.title : '', '暂无周报')
setText($summary, report ? report.summary : '', '暂无摘要')
setHtml($content, report ? report.content : '', '暂无内容')
}
const renderEvents = (events) => {
if (!$events) return
$events.innerHTML = ''
if (!events.length) {
const empty = document.createElement('div')
empty.className = 'empty'
empty.textContent = '暂无事件'
$events.appendChild(empty)
return
}
events.forEach(ev => {
const wrap = document.createElement('div')
wrap.className = 'event'
const header = document.createElement('div')
header.className = 'event-header'
const tag = document.createElement('span')
tag.className = 'tag'
tag.textContent = statusLabel(ev.status_code)
const title = document.createElement('div')
title.className = 'event-title'
title.textContent = buildEventTitleByPolicy(ev.title || '', ev.content || '')
header.appendChild(tag)
header.appendChild(title)
const meta = document.createElement('div')
meta.className = 'event-meta'
if (ev.deadline) {
const item = document.createElement('div')
item.className = 'event-meta-item'
item.textContent = `截止时间:${formatCNDeadline(ev.deadline)}`
meta.appendChild(item)
}
const pushItem = document.createElement('div')
pushItem.className = 'event-meta-item'
pushItem.textContent = `推送:${ev.push_required ? '是' : '否'}`
meta.appendChild(pushItem)
const body = document.createElement('div')
body.className = 'event-body'
body.innerHTML = sanitizeHtml(ev.content || '')
wrap.appendChild(header)
wrap.appendChild(meta)
wrap.appendChild(body)
$events.appendChild(wrap)
})
}
const loadEvents = async () => {
if (!$weeks.value) return
addLoadingStep(`查询周报 ${$weeks.value}`, '🔍')
const reportData = await fetchJson('/report', { week_id: $weeks.value })
const report = reportData.report || null
if (!report || !report.id) {
addLoadingStep('本周暂无周报或事件', 'ℹ️')
renderEvents([])
return
}
addLoadingStep(`获取事件列表 report_id=${report.id}`, '📑')
const eventsData = await fetchJson('/events', { report_id: report.id })
const events = eventsData.rows || eventsData.events || []
addLoadingStep(`渲染事件 ${events.length} 条`, '🧩')
renderEvents(events)
}
const loadCurrent = async () => {
setStatus('')
try {
addLoadingStep(`进入${page === 'report' ? '周报' : '事件列表'}页面`, '📌')
if (page === 'report') await loadReport()
else await loadEvents()
} catch (e) {
setStatus(String(e.message || e || '加载失败'))
}
}
const movePrevWeek = () => {
const idx = weekList.findIndex(w => w.week_id === $weeks.value)
if (idx >= 0 && idx < weekList.length - 1) {
$weeks.value = weekList[idx + 1].week_id
updateWeekButtons()
runWithLoading(loadCurrent, '切换上一周')
}
}
const moveNextWeek = () => {
const idx = weekList.findIndex(w => w.week_id === $weeks.value)
if (idx > 0) {
$weeks.value = weekList[idx - 1].week_id
updateWeekButtons()
runWithLoading(loadCurrent, '切换下一周')
}
}
if (!accessKey) {
setNavLinks()
setStatus('缺少访问 key,请在 URL 中添加 ?key=你的key')
if ($year) $year.disabled = true
if ($weeks) $weeks.disabled = true
if ($prev) $prev.disabled = true
if ($next) $next.disabled = true
return
}
persistKey()
setNavLinks()
if ($btnCopy) $btnCopy.onclick = () => { copyLogs() }
if ($year) $year.onchange = () => {
currentYear = parseInt($year.value, 10) || currentYear
runWithLoading(() => loadWeeks(currentYear), '切换年份')
}
if ($weeks) $weeks.onchange = () => {
updateWeekButtons()
runWithLoading(loadCurrent, '切换周')
}
if ($prev) $prev.onclick = movePrevWeek
if ($next) $next.onclick = moveNextWeek
if ($refreshReport) $refreshReport.onclick = () => runWithLoading(loadReport, '刷新周报')
if ($refreshEvents) $refreshEvents.onclick = () => runWithLoading(loadEvents, '刷新事件')
if ($overlay) {
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return
if ($overlay.style.display !== 'flex') return
if (!$btnClose || !$btnClose.disabled) $overlay.style.display = 'none'
})
}
runWithLoading(loadYears, '初始化加载')
})()