// ===== 从 index.php 迁移的脚本(中文状态版) ===== const Statuses = ["待做","进行","完成","异常"]; // 四种状态 let Projects = []; // 全量项目数据 let CurrentProjectId = null; const ShowStep = 15; // 每列初始显示数量 let ShowLimits = { '待做': ShowStep, '进行': ShowStep, '完成': ShowStep, '异常': ShowStep }; let Filters = { status: '全部', search: '', month: '' }; // month: YYYY-MM // 获取项目的年月键(优先 created_at,其次 updated_at) function projectMonthKey(p) { return monthKeyFromDateStr(p?.created_at || p?.updated_at || '') || null; } const api = async (url, options = {}) => { const res = await fetch(url, options); const ct = res.headers.get('Content-Type') || ''; if (ct.includes('application/json')) { try { const data = await res.json(); if (data && data.need_login) { // 统一跳回登录页 location.href = 'index.php'; throw new Error('未登录'); } return data; } catch (e) { // JSON 解析异常时直接返回空 return null; } } return res.text(); }; // 退出登录 async function logout() { try { const data = await api('api.php?action=logout', { method: 'POST' }); // 清理本地标记 try { localStorage.removeItem('auth_ok'); } catch (e) {} // 返回登录页 location.href = 'index.php'; } catch (e) { // 即便异常也尝试跳转 location.href = 'index.php'; } } async function loadProjects() { const data = await api('api.php?action=list_projects'); Projects = Array.isArray(data?.projects) ? data.projects : []; // 每次加载数据时重置显示数量(避免越来越多) ShowLimits = { '待做': ShowStep, '进行': ShowStep, '完成': ShowStep, '异常': ShowStep }; renderBoard(); renderMonthlyChart(); } function renderBoard() { const board = document.getElementById('statusBoard'); board.innerHTML = ''; const statusesToRender = Filters.status === '全部' ? Statuses : [Filters.status]; const q = (Filters.search || '').trim().toLowerCase(); statusesToRender.forEach(st => { const items = Projects.filter(p => p.status === st); let filtered = items.filter(p => { if (!q) return true; return String(p.name || '').toLowerCase().includes(q); }); if (Filters.month) { filtered = filtered.filter(p => projectMonthKey(p) === Filters.month); } const col = document.createElement('div'); col.className = 'column'; col.dataset.status = st; // 用于拖拽目标识别 col.innerHTML = `

${st} (${filtered.length})

`; // 列拖拽目标事件 col.addEventListener('dragover', (e) => { e.preventDefault(); col.classList.add('drag-over'); }); col.addEventListener('dragleave', () => { col.classList.remove('drag-over'); }); col.addEventListener('drop', (e) => { e.preventDefault(); col.classList.remove('drag-over'); const pid = e.dataTransfer.getData('text/plain'); if (pid) { updateProject(pid, { status: st }); } }); const limit = ShowLimits[st] || ShowStep; filtered.slice(0, limit).forEach(p => { const div = document.createElement('div'); div.className = 'project'; div.draggable = true; div.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', p.id); e.dataTransfer.effectAllowed = 'move'; div.classList.add('dragging'); }); div.addEventListener('dragend', () => { div.classList.remove('dragging'); }); const title = document.createElement('div'); title.className = 'title'; title.textContent = p.name; title.style.cursor = 'pointer'; title.onclick = () => openProject(p.id); const actions = document.createElement('div'); actions.className = 'actions'; const sel = document.createElement('select'); sel.className = 'select'; Statuses.forEach(s => { const opt = document.createElement('option'); opt.value = s; opt.textContent = s; if (s === p.status) opt.selected = true; sel.appendChild(opt); }); sel.onchange = () => updateProject(p.id, { status: sel.value }); const editBtn = document.createElement('button'); editBtn.className = 'btn'; editBtn.textContent = '编辑'; editBtn.onclick = () => openProject(p.id); const delBtn = document.createElement('button'); delBtn.className = 'btn danger'; delBtn.textContent = '删除'; delBtn.onclick = () => deleteProject(p.id); actions.appendChild(sel); actions.appendChild(editBtn); actions.appendChild(delBtn); div.appendChild(title); div.appendChild(actions); col.appendChild(div); }); if (filtered.length > limit) { const more = document.createElement('button'); more.className = 'btn'; more.textContent = `显示更多... (${filtered.length - limit})`; more.onclick = () => { ShowLimits[st] = limit + ShowStep; renderBoard(); }; col.appendChild(more); } board.appendChild(col); }); } async function createProject() { const nameInput = document.getElementById('newProjectName'); const name = (nameInput.value || '').trim(); if (!name) { alert('请输入项目名称'); return; } const resp = await api('api.php?action=create_project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); if (resp && resp.ok) { nameInput.value=''; await loadProjects(); } else { alert(resp?.message || '创建失败'); } } async function updateProject(id, updates) { const resp = await api('api.php?action=update_project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, ...updates }) }); if (resp && resp.ok) { await loadProjects(); } else { alert(resp?.message || '更新失败'); } } async function deleteProject(id) { if (!confirm('确认删除该项目及其笔记吗?')) return; const resp = await api('api.php?action=delete_project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); if (resp && resp.ok) { await loadProjects(); } else { alert(resp?.message || '删除失败'); } } async function openProject(id) { // 改为跳转到独立详情页,不再使用悬浮弹窗 location.href = 'project.php?id=' + encodeURIComponent(id); } function closeProjectModal() { document.getElementById('projectModalMask').style.display = 'none'; CurrentProjectId = null; } function renderNotes(notes) { const list = document.getElementById('notesList'); list.innerHTML = ''; notes.forEach(n => { const div = document.createElement('div'); div.className = 'note'; // 预览在上方:评论气泡样式 const preview = document.createElement('div'); preview.className = 'note-preview note-bubble'; const ta = document.createElement('textarea'); ta.className = 'input'; ta.style.width = '100%'; ta.style.minHeight = '80px'; ta.value = n.content || ''; preview.innerHTML = noteTextToHtml(ta.value || ''); enhancePreview(preview); const meta = document.createElement('div'); meta.className = 'meta'; meta.textContent = `创建: ${n.created_at || ''} 更新: ${n.updated_at || ''}`; const bar = document.createElement('div'); bar.style.display = 'flex'; bar.style.gap='8px'; bar.style.marginTop='6px'; const save = document.createElement('button'); save.className = 'btn'; save.textContent = '保存修改'; save.onclick = () => updateNote(n.id, ta.value); const del = document.createElement('button'); del.className = 'btn danger'; del.textContent = '删除'; del.onclick = () => deleteNote(n.id); const addImgBtn = document.createElement('button'); addImgBtn.className = 'btn'; addImgBtn.textContent = '添加图片'; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.className = 'hidden'; addImgBtn.onclick = () => fileInput.click(); fileInput.onchange = async () => { const f = fileInput.files && fileInput.files[0]; if (!f) return; const up = await uploadFile(f); if (up && up.ok && up.url) { const md = `\n![图片](${up.url})\n`; insertAtCursor(ta, md); preview.innerHTML = noteTextToHtml(ta.value || ''); enhancePreview(preview); fileInput.value = ''; } else { alert(up?.message || '图片上传失败'); } }; ta.addEventListener('input', () => { preview.innerHTML = noteTextToHtml(ta.value || ''); enhancePreview(preview); }); bar.appendChild(save); bar.appendChild(del); bar.appendChild(addImgBtn); bar.appendChild(fileInput); // 排列顺序:预览气泡 -> 元信息 -> 编辑框 -> 操作条 div.appendChild(preview); div.appendChild(meta); div.appendChild(ta); div.appendChild(bar); list.appendChild(div); // 允许直接粘贴或拖拽图片/文件到笔记文本框 ta.addEventListener('paste', async (e) => { const items = e.clipboardData?.items || []; const files = []; for (const it of items) { if (it.kind === 'file') { const f = it.getAsFile(); if (f) files.push(f); } } if (files.length) { e.preventDefault(); await handleFilesInsert(ta, files); preview.innerHTML = noteTextToHtml(ta.value || ''); enhancePreview(preview); } }); ta.addEventListener('dragover', (e) => { e.preventDefault(); }); ta.addEventListener('drop', async (e) => { e.preventDefault(); const files = Array.from(e.dataTransfer?.files || []); if (files.length) { await handleFilesInsert(ta, files); preview.innerHTML = noteTextToHtml(ta.value || ''); enhancePreview(preview); } }); }); } async function applyProjectMeta() { if (!CurrentProjectId) return; const name = document.getElementById('renameProjectInput').value.trim(); const status = document.getElementById('statusSelect').value; const resp = await api('api.php?action=update_project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: CurrentProjectId, name, status }) }); const msg = document.getElementById('modalMsg'); if (resp && resp.ok) { msg.textContent = '项目已保存'; await loadProjects(); openProject(CurrentProjectId); } else { msg.textContent = resp?.message || '保存失败'; } } async function deleteCurrentProject() { if (CurrentProjectId) { await deleteProject(CurrentProjectId); closeProjectModal(); } } async function addNote() { if (!CurrentProjectId) return; const content = document.getElementById('newNoteInput').value.trim(); if (!content) { alert('请输入笔记内容'); return; } const resp = await api('api.php?action=add_note', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: CurrentProjectId, content }) }); if (resp && resp.ok) { document.getElementById('newNoteInput').value=''; await loadProjects(); openProject(CurrentProjectId); } else { alert(resp?.message || '添加失败'); } } async function updateNote(note_id, content) { if (!CurrentProjectId) return; const resp = await api('api.php?action=update_note', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: CurrentProjectId, note_id, content }) }); if (resp && resp.ok) { await loadProjects(); openProject(CurrentProjectId); } else { alert(resp?.message || '保存失败'); } } async function deleteNote(note_id) { if (!CurrentProjectId) return; if (!confirm('确认删除该笔记吗?')) return; const resp = await api('api.php?action=delete_note', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: CurrentProjectId, note_id }) }); if (resp && resp.ok) { await loadProjects(); openProject(CurrentProjectId); } else { alert(resp?.message || '删除失败'); } } async function exportProjectHtml() { if (!CurrentProjectId) return; const resp = await api('api.php?action=export_project_html&id='+encodeURIComponent(CurrentProjectId)); if (resp && resp.ok && resp.url) { window.open(resp.url, '_blank'); // 导出后刷新“导出管理”列表 await loadExports(); } else { alert(resp?.message || '导出失败'); } } // 主动清理未使用的上传文件(后台扫描 data/uploads 与所有笔记引用) async function cleanupUnusedUploads() { const resp = await api('api.php?action=cleanup_unused_uploads'); if (resp && resp.ok) { const d = resp.cleanup?.deleted ?? 0; alert(`清理完成:删除未引用文件 ${d} 个`); await loadProjects(); if (CurrentProjectId) openProject(CurrentProjectId); } else { alert(resp?.message || '清理失败'); } } // 文件上传(图片) async function uploadFile(file) { const fd = new FormData(); fd.append('file', file); return api('api.php?action=upload_file', { method: 'POST', body: fd }); } // 将笔记文本转换为HTML(支持 Markdown 图片:![alt](url)) function noteTextToHtml(text) { if (!text) return ''; const imgReg = /!\[([^\]]*)\]\(([^\)]+)\)/u; const linkReg = /\[([^\]]+)\]\(([^\)]+)\)/u; // 链接(图片将由 imgReg 优先匹配) let out = ''; let pos = 0; while (true) { const imgM = imgReg.exec(text.slice(pos)); const linkM = linkReg.exec(text.slice(pos)); const imgPos = imgM ? imgM.index : -1; const linkPos = linkM ? linkM.index : -1; if (imgPos < 0 && linkPos < 0) break; const nextRel = (imgPos >=0 && (linkPos < 0 || imgPos <= linkPos)) ? imgPos : linkPos; out += escapeHtml(text.slice(pos, pos + nextRel)).replace(/\n/g, '
'); if (nextRel === imgPos) { const alt = escapeHtml(imgM[1] || ''); const url = escapeHtml(imgM[2] || ''); out += `${alt}`; pos += imgPos + imgM[0].length; } else { const label = escapeHtml(linkM[1] || ''); const urlRaw = linkM[2] || ''; const url = escapeHtml(urlRaw); if (isImageUrl(urlRaw)) { out += `${label}`; } else { out += `${label}`; } pos += linkPos + linkM[0].length; } } out += escapeHtml(text.slice(pos)).replace(/\n/g, '
'); return out; } function escapeHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function insertAtCursor(ta, text) { const start = ta.selectionStart ?? ta.value.length; const end = ta.selectionEnd ?? start; ta.value = ta.value.slice(0, start) + text + ta.value.slice(end); ta.focus(); ta.selectionStart = ta.selectionEnd = start + text.length; ta.dispatchEvent(new Event('input')); } // 预览增强:点击图片在新窗口查看原图 function enhancePreview(previewEl) { previewEl.querySelectorAll('img').forEach(img => { img.addEventListener('click', () => { openImageLightbox(img.src, img.alt || ''); }); }); } function isImageFile(file) { const t = (file?.type || '').toLowerCase(); if (t.startsWith('image/')) return true; const name = file?.name || ''; return /\.(png|jpg|jpeg|gif|webp)$/i.test(name); } async function handleFilesInsert(ta, files) { for (const f of files) { const up = await uploadFile(f); if (up && up.ok && up.url) { const fname = (up.name || f.name || '附件'); const url = up.url; const md = isImageFile(f) ? `\n![${fname}](${url})\n` : `\n[${fname}](${url})\n`; insertAtCursor(ta, md); } else { alert(up?.message || `上传失败:${f.name}`); } } } function isImageUrl(url) { return /\.(png|jpg|jpeg|gif|webp)(\?.*)?$/i.test(url) || /^data:image\//i.test(url); } // ====== 图片浮窗(Lightbox) ====== let _imgLightboxEl = null; function ensureImageLightbox() { if (_imgLightboxEl) return _imgLightboxEl; const mask = document.createElement('div'); mask.className = 'img-lightbox-mask'; const content = document.createElement('div'); content.className = 'img-lightbox-content'; const img = document.createElement('img'); const closeBtn = document.createElement('button'); closeBtn.className = 'img-lightbox-close'; closeBtn.textContent = '×'; content.appendChild(img); content.appendChild(closeBtn); mask.appendChild(content); document.body.appendChild(mask); // 交互 const close = () => { mask.style.display = 'none'; document.body.style.overflow = ''; document.removeEventListener('keydown', escHandler); }; const escHandler = (e) => { if (e.key === 'Escape') close(); }; mask.addEventListener('click', close); content.addEventListener('click', (e) => e.stopPropagation()); closeBtn.addEventListener('click', close); _imgLightboxEl = mask; return _imgLightboxEl; } function openImageLightbox(src, alt) { const el = ensureImageLightbox(); const img = el.querySelector('img'); img.src = src; img.alt = alt || ''; el.style.display = 'flex'; document.body.style.overflow = 'hidden'; const escHandler = (e) => { if (e.key === 'Escape') { el.style.display='none'; document.body.style.overflow=''; document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } // ====== 月份曲线图 ====== let chartInstance = null; function getLast12MonthsLabels() { const labels = []; const now = new Date(); for (let i = 11; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); const y = d.getFullYear(); const m = (d.getMonth() + 1).toString().padStart(2, '0'); labels.push(`${y}-${m}`); } return labels; } function monthKeyFromDateStr(str) { // 输入格式:YYYY-MM-DD HH:mm:ss if (!str || typeof str !== 'string' || str.length < 7) return null; const m = str.slice(0,7); // 基础校验 if (!/^\d{4}-\d{2}$/.test(m)) return null; return m; } function computeMonthlySeries(labels) { const colorMap = { '待做': '#3b82f6', '进行': '#f59e0b', '完成': '#10b981', '异常': '#ef4444' }; const series = {}; Statuses.forEach(st => { series[st] = labels.map(_ => 0); }); Projects.forEach(p => { // 优先使用创建月份;没有则使用更新时间;都没有则归入当前月 const mk = monthKeyFromDateStr(p.created_at || p.updated_at || '') || labels[labels.length - 1]; if (!mk) return; // 略过缺少时间的项目 const idx = labels.indexOf(mk); if (idx === -1) return; // 不在最近12个月 const st = p.status || '待做'; if (!series[st]) series[st] = labels.map(_ => 0); series[st][idx] += 1; }); const datasets = Statuses.map(st => ({ label: st, data: series[st], tension: 0.25, borderColor: colorMap[st], backgroundColor: colorMap[st], pointRadius: 3, borderWidth: 2, fill: false, })); return datasets; } function drawSimpleChart(canvas, labels, datasets) { const dpr = window.devicePixelRatio || 1; const cw = canvas.clientWidth || 600; const ch = canvas.clientHeight || 140; canvas.width = Math.floor(cw * dpr); canvas.height = Math.floor(ch * dpr); const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); const margin = { left: 36, right: 12, top: 12, bottom: 28 }; const plotW = cw - margin.left - margin.right; const plotH = ch - margin.top - margin.bottom; // 背景 ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, cw, ch); // 计算最大值 let maxY = 0; datasets.forEach(ds => ds.data.forEach(v => { if (v > maxY) maxY = v; })); if (maxY === 0) maxY = 5; const stepX = labels.length > 1 ? plotW / (labels.length - 1) : plotW; const scaleY = plotH / maxY; // 坐标轴 ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(margin.left, margin.top); ctx.lineTo(margin.left, margin.top + plotH); ctx.lineTo(margin.left + plotW, margin.top + plotH); ctx.stroke(); // y 轴刻度 ctx.fillStyle = '#6b7280'; ctx.font = '12px Arial'; for (let i = 0; i <= 4; i++) { const yVal = Math.round((maxY * i) / 4); const y = margin.top + plotH - yVal * scaleY; ctx.fillText(String(yVal), 6, y + 4); ctx.strokeStyle = '#f3f4f6'; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(margin.left + plotW, y); ctx.stroke(); } // x 轴标签(每隔2个月显示一次) for (let i = 0; i < labels.length; i += Math.ceil(labels.length / 6)) { const x = margin.left + i * stepX; ctx.fillStyle = '#6b7280'; ctx.save(); ctx.translate(x, margin.top + plotH + 14); ctx.rotate(-Math.PI / 8); ctx.fillText(labels[i], 0, 0); ctx.restore(); } // 折线 datasets.forEach(ds => { ctx.strokeStyle = ds.borderColor; ctx.lineWidth = 2; ctx.beginPath(); for (let i = 0; i < ds.data.length; i++) { const x = margin.left + i * stepX; const y = margin.top + plotH - ds.data[i] * scaleY; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); // 点 ctx.fillStyle = ds.borderColor; for (let i = 0; i < ds.data.length; i++) { const x = margin.left + i * stepX; const y = margin.top + plotH - ds.data[i] * scaleY; ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill(); } }); // 简易图例 let lx = margin.left; let ly = margin.top - 2; datasets.forEach(ds => { ctx.fillStyle = ds.borderColor; ctx.fillRect(lx, ly, 10, 10); ctx.fillStyle = '#374151'; ctx.fillText(ds.label, lx + 14, ly + 10); lx += 60; }); } function renderMonthlyChart() { const labels = getLast12MonthsLabels(); const datasets = computeMonthlySeries(labels); const canvas = document.getElementById('monthlyChart'); const msg = document.getElementById('chartMsg'); if (window.Chart) { const ctx = canvas.getContext('2d'); if (chartInstance) { chartInstance.destroy(); } chartInstance = new Chart(ctx, { type: 'line', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, // 父容器固定高度,避免在Flex布局下无限增高 plugins: { legend: { position: 'bottom' }, tooltip: { mode: 'index', intersect: false } }, interaction: { mode: 'nearest', axis: 'x', intersect: false }, scales: { y: { beginAtZero: true, ticks: { precision:0 } }, x: { grid: { display: false } } } } }); msg.textContent = ''; } else { // 无法加载外部CDN时,使用内置简易绘制 msg.textContent = '图表库未加载(可能网络限制或被拦截),已切换为内置简易曲线图。'; drawSimpleChart(canvas, labels, datasets); } } // 初次加载 loadProjects(); // 对外暴露(给内联按钮用) window.createProject = createProject; window.updateProject = updateProject; window.deleteProject = deleteProject; window.openProject = openProject; window.closeProjectModal = closeProjectModal; window.applyProjectMeta = applyProjectMeta; window.deleteCurrentProject = deleteCurrentProject; window.addNote = addNote; window.updateNote = updateNote; window.deleteNote = deleteNote; window.exportProjectHtml = exportProjectHtml; window.cleanupUnusedUploads = cleanupUnusedUploads; // 绑定筛选与搜索(在脚本加载后执行一次) document.addEventListener('DOMContentLoaded', () => { const sel = document.getElementById('statusFilterSelect'); if (sel) sel.addEventListener('change', () => { Filters.status = sel.value || '全部'; renderBoard(); }); const input = document.getElementById('projectSearchInput'); if (input) input.addEventListener('input', () => { Filters.search = input.value || ''; renderBoard(); }); const monthInput = document.getElementById('monthFilterInput'); if (monthInput) monthInput.addEventListener('change', () => { Filters.month = monthInput.value || ''; renderBoard(); }); const clearBtn = document.getElementById('monthFilterClearBtn'); if (clearBtn) clearBtn.addEventListener('click', () => { Filters.month=''; if (monthInput) monthInput.value=''; renderBoard(); }); // 新增笔记条中的“添加图片”支持 const newNoteBtn = document.getElementById('newNoteAddImageBtn'); const newNoteFile = document.getElementById('newNoteImageInput'); const newNoteInput = document.getElementById('newNoteInput'); if (newNoteBtn && newNoteFile && newNoteInput) { newNoteBtn.addEventListener('click', () => newNoteFile.click()); newNoteFile.addEventListener('change', async () => { const f = newNoteFile.files && newNoteFile.files[0]; if (!f) return; const up = await uploadFile(f); if (up && up.ok && up.url) { const md = isImageFile(f) ? `\n![${up.name || f.name || '图片'}](${up.url})\n` : `\n[${up.name || f.name || '附件'}](${up.url})\n`; newNoteInput.value = (newNoteInput.value || '') + md; newNoteFile.value = ''; } else { alert(up?.message || '图片上传失败'); } }); // 允许粘贴/拖拽到新增笔记输入框 newNoteInput.addEventListener('paste', async (e) => { const items = e.clipboardData?.items || []; const files = []; for (const it of items) { if (it.kind === 'file') { const f = it.getAsFile(); if (f) files.push(f); } } if (files.length) { e.preventDefault(); await handleFilesInsert(newNoteInput, files); } }); newNoteInput.addEventListener('dragover', (e) => { e.preventDefault(); }); newNoteInput.addEventListener('drop', async (e) => { e.preventDefault(); const files = Array.from(e.dataTransfer?.files || []); if (files.length) { await handleFilesInsert(newNoteInput, files); } }); } }); // ====== 导出管理 ====== let ExportFilesCache = []; let ExportsFilters = { search: '', month: '' }; async function loadExports() { const resp = await api('api.php?action=list_exports'); ExportFilesCache = Array.isArray(resp?.files) ? resp.files : []; renderExports(ExportFilesCache); } function renderExports(files) { const box = document.getElementById('exportsList'); if (!box) return; // 过滤 const q = (ExportsFilters.search || '').trim().toLowerCase(); const month = ExportsFilters.month || ''; let filtered = files.slice(); if (q) filtered = filtered.filter(f => String(f.name || '').toLowerCase().includes(q)); if (month) filtered = filtered.filter(f => monthKeyFromDateStr(f.mtime || '') === month); // 数量统计 const countEl = document.getElementById('exportsCount'); if (countEl) countEl.textContent = `共 ${filtered.length} 项${month ? ` · 月份:${month}` : ''}${q ? ` · 关键词:${q}` : ''}`; if (!filtered.length) { box.innerHTML = '
暂无匹配的导出文件
'; return; } box.innerHTML = ''; filtered.forEach(f => { const row = document.createElement('div'); row.className = 'export-item'; const info = document.createElement('div'); info.className = 'export-info'; const sizeKB = Math.round((f.size || 0) / 1024); info.textContent = `${f.name} · ${f.mtime} · ${sizeKB} KB`; const acts = document.createElement('div'); acts.className = 'export-actions'; const openBtn = document.createElement('button'); openBtn.className = 'btn'; openBtn.textContent = '打开'; openBtn.onclick = () => window.open(f.url, '_blank'); const shareBtn = document.createElement('button'); shareBtn.className = 'btn'; shareBtn.textContent = '分享'; shareBtn.onclick = async () => { const abs = new URL(f.url, window.location.href).href; try { await navigator.clipboard.writeText(abs); alert('已复制链接到剪贴板:\n' + abs); } catch(e) { prompt('复制失败,请手动复制:', abs); } }; const delBtn = document.createElement('button'); delBtn.className = 'btn danger'; delBtn.textContent = '删除'; delBtn.onclick = async () => { if (!confirm(`确认删除导出文件:${f.name}?`)) return; const r = await api('api.php?action=delete_export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: f.name }) }); if (r && r.ok) { await loadExports(); } else { alert(r?.message || '删除失败'); } }; acts.appendChild(openBtn); acts.appendChild(shareBtn); acts.appendChild(delBtn); row.appendChild(info); row.appendChild(acts); box.appendChild(row); }); } // 初始加载导出列表 document.addEventListener('DOMContentLoaded', () => { // 初次加载 loadExports(); // 绑定筛选控件 const searchInput = document.getElementById('exportsSearchInput'); if (searchInput) searchInput.addEventListener('input', () => { ExportsFilters.search = searchInput.value || ''; renderExports(ExportFilesCache); }); const monthInput = document.getElementById('exportsMonthInput'); if (monthInput) monthInput.addEventListener('change', () => { ExportsFilters.month = monthInput.value || ''; renderExports(ExportFilesCache); }); const clearBtn = document.getElementById('exportsMonthClearBtn'); if (clearBtn) clearBtn.addEventListener('click', () => { ExportsFilters.month=''; if (monthInput) monthInput.value=''; renderExports(ExportFilesCache); }); });