Files
XinagMuKanBan/assets/js/app.js
2025-11-18 14:28:07 +08:00

690 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ===== 从 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 = `<h3>${st} <span class="count">(${filtered.length})</span></h3>`;
// 列拖拽目标事件
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, '<br>');
if (nextRel === imgPos) {
const alt = escapeHtml(imgM[1] || '');
const url = escapeHtml(imgM[2] || '');
out += `<img src="${url}" alt="${alt}">`;
pos += imgPos + imgM[0].length;
} else {
const label = escapeHtml(linkM[1] || '');
const urlRaw = linkM[2] || '';
const url = escapeHtml(urlRaw);
if (isImageUrl(urlRaw)) {
out += `<img src="${url}" alt="${label}">`;
} else {
out += `<a href="${url}" target="_blank" rel="noopener noreferrer">${label}</a>`;
}
pos += linkPos + linkM[0].length;
}
}
out += escapeHtml(text.slice(pos)).replace(/\n/g, '<br>');
return out;
}
function escapeHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
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 = '<div class="muted">暂无匹配的导出文件</div>'; 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); });
});