// 项目详情页脚本(不使用悬浮弹窗) 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) { return null; } } return res.text(); }; let CurrentProjectId = null; let CurrentProject = null; function escapeHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function isImageUrl(url){ return /\.(png|jpg|jpeg|gif|webp)(\?.*)?$/i.test(url) || /^data:image\//i.test(url); } // 将笔记文本转换为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 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 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); } function enhancePreview(previewEl) { previewEl.querySelectorAll('img').forEach(img => { img.addEventListener('click', () => { openImageLightbox(img.src, img.alt || ''); }); }); } // ====== 图片浮窗(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); } async function uploadFile(file) { const fd = new FormData(); fd.append('file', file); return api('api.php?action=upload_file', { method: 'POST', body: fd }); } async function loadProject() { const id = window.PAGE_PROJECT_ID || ''; if (!id) { document.getElementById('pageMsg').textContent = '缺少项目ID'; return; } const data = await api('api.php?action=get_project&id=' + encodeURIComponent(id)); if (data && data.ok) { CurrentProject = data.project; CurrentProjectId = CurrentProject.id; document.getElementById('pageProjectTitle').textContent = CurrentProject.name; document.getElementById('pageProjectStatus').textContent = '· 当前状态:' + CurrentProject.status; document.getElementById('renameProjectInput').value = CurrentProject.name; document.getElementById('statusSelect').value = CurrentProject.status; // ETA 输入显示控制(仅进行中显示) const etaInput = document.getElementById('etaInput'); if (etaInput) { const key = 'project_eta_' + CurrentProjectId; const saved = localStorage.getItem(key) || ''; etaInput.style.display = (CurrentProject.status === '进行') ? '' : 'none'; if (saved) etaInput.value = saved; } updateProjectReminder(); renderNotes(CurrentProject.notes || []); } else { document.getElementById('pageMsg').textContent = (data && data.message) ? data.message : '加载失败'; } } function timeAgo(str) { if (!str) return ''; const d = new Date(str.replace(/-/g,'/')); // iOS 兼容 if (isNaN(d.getTime())) return str; const diff = Date.now() - d.getTime(); const sec = Math.floor(diff/1000); if (sec < 60) return sec + '秒前'; const min = Math.floor(sec/60); if (min < 60) return min + '分钟前'; const hour = Math.floor(min/60); if (hour < 24) return hour + '小时前'; const day = Math.floor(hour/24); if (day < 30) return day + '天前'; const month = Math.floor(day/30); if (month < 12) return month + '个月前'; const year = Math.floor(month/12); return year + '年前'; } function durationSince(str) { if (!str) return ''; const d = new Date(str.replace(/-/g,'/')); if (isNaN(d.getTime())) return ''; let ms = Date.now() - d.getTime(); if (ms < 0) ms = 0; const days = Math.floor(ms / (24*3600*1000)); ms -= days * 24*3600*1000; const hours = Math.floor(ms / (3600*1000)); ms -= hours * 3600*1000; const mins = Math.floor(ms / (60*1000)); const parts = []; if (days > 0) parts.push(days + '天'); if (hours > 0) parts.push(hours + '小时'); if (days === 0 && mins > 0) parts.push(mins + '分钟'); return parts.join('') || '刚刚'; } function updateProjectReminder() { try { const box = document.getElementById('projectRemind'); if (!box || !CurrentProject) return; const p = CurrentProject; let msg = ''; if (p.status === '异常') { const base = p.updated_at || p.created_at || ''; msg = `异常状态已持续:${durationSince(base)}。请尽快处理。`; } else if (p.status === '待做') { const base = p.created_at || ''; msg = `项目创建至今已等待:${durationSince(base)}。建议尽快开始。`; } else if (p.status === '进行') { const base = p.created_at || p.updated_at || ''; const howlong = durationSince(base); const key = 'project_eta_' + p.id; const eta = localStorage.getItem(key) || ''; if (eta) { const ed = new Date(eta + 'T00:00:00'); const now = new Date(); let diff = ed.getTime() - now.getTime(); const daysLeft = Math.ceil(diff / (24*3600*1000)); const tail = daysLeft >= 0 ? `剩余约 ${daysLeft} 天` : `已超期 ${Math.abs(daysLeft)} 天`; msg = `进行中:已用时 ${howlong};计划完成:${eta}(${tail})。`; } else { msg = `进行中:已用时 ${howlong}。可设置“计划完成日期”以显示剩余天数。`; } } else if (p.status === '完成') { const base = p.updated_at || p.created_at || ''; msg = `已完成(${timeAgo(base)})。`; } // 原始提醒文案(用于 AI 的原始输入) box.textContent = msg; box.setAttribute('data-raw', msg); // 尝试从后端获取已保存的AI润色文本(持久化在服务器JSON) (async () => { try { const resp = await api('api.php?action=get_ai_reminder&id=' + encodeURIComponent(CurrentProjectId)); if (resp && resp.ok && resp.text) { box.textContent = resp.text; } } catch (e) {} })(); } catch (e) {} } // ====== AI 自动润色提醒 ====== async function aiEnhanceReminderOnce() { try { const box = document.getElementById('projectRemind'); if (!box || !CurrentProjectId) return; const raw = (box.getAttribute('data-raw') || box.textContent || '').trim(); if (!raw) return; // 进度提示:结合笔记进行润色 box.textContent = 'AI正在结合笔记进行润色…'; const lastKey = 'ai_last_ts_' + CurrentProjectId; const resp = await api('api.php?action=ai_enrich_reminder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ raw: raw, project: { id: CurrentProjectId, name: CurrentProject?.name, status: CurrentProject?.status }, include_notes: true }) }); if (resp && resp.ok && resp.text) { box.textContent = resp.text; try { localStorage.setItem(lastKey, String(Date.now())); } catch (e) {} } } catch (e) { /* 静默失败 */ } } // 已取消自动AI润色,改为手动点击按钮触发 function firstLetter(name){ return (name || '记').trim().slice(0,1).toUpperCase(); } function renderNotes(notes) { const list = document.getElementById('notesList'); list.innerHTML = ''; notes.forEach(n => { // Git风格卡片容器 const card = document.createElement('div'); card.className = 'note-git'; const header = document.createElement('div'); header.className = 'note-git-header'; const titleEl = document.createElement('span'); titleEl.className = 'note-git-title'; titleEl.textContent = '笔记'; titleEl.title = '点击以展开/收起编辑'; const timeEl = document.createElement('span'); timeEl.className = 'note-git-time'; timeEl.textContent = timeAgo(n.created_at || '') || (n.created_at || ''); header.appendChild(titleEl); header.appendChild(timeEl); // 预览(Git风格正文) const preview = document.createElement('div'); preview.className = 'note-git-body'; 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 editWrap = document.createElement('div'); editWrap.style.marginTop = '6px'; editWrap.appendChild(ta); editWrap.classList.add('hidden'); // 底部操作:左侧图标,右侧保存/删除/添加图片 const footer = document.createElement('div'); footer.className = 'note-footer'; const actionsRight = document.createElement('div'); actionsRight.className = 'note-actions-right'; 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?.[0]; if (!f) return; if (!isImageFile(f)) { alert('请选择图片文件'); return; } const up = await uploadFile(f); if (up && up.ok && up.url) { insertAtCursor(ta, `![image](${up.url})`); preview.innerHTML = noteTextToHtml(ta.value || ''); enhancePreview(preview); } else { alert(up?.message || '上传失败'); } fileInput.value = ''; }; ta.addEventListener('input', () => { preview.innerHTML = noteTextToHtml(ta.value || ''); enhancePreview(preview); }); // 点击标题“笔记”展开/收起编辑;双击正文也可展开编辑 const toggleEdit = () => { const hidden = editWrap.classList.contains('hidden'); if (hidden) { editWrap.classList.remove('hidden'); footer.classList.remove('hidden'); } else { editWrap.classList.add('hidden'); footer.classList.add('hidden'); } }; titleEl.addEventListener('click', toggleEdit); preview.addEventListener('dblclick', toggleEdit); actionsRight.appendChild(save); actionsRight.appendChild(del); actionsRight.appendChild(addImgBtn); actionsRight.appendChild(fileInput); footer.appendChild(actionsRight); // 初始隐藏底部按钮,跟随编辑区一起展开/收起 footer.classList.add('hidden'); // 组装卡片:头部 -> 预览 -> 编辑(默认展开) -> 底部 card.appendChild(header); card.appendChild(preview); card.appendChild(editWrap); card.appendChild(footer); list.appendChild(card); }); } 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('pageMsg'); if (resp && resp.ok) { msg.textContent = '项目已保存'; await loadProject(); } else { msg.textContent = resp?.message || '保存失败'; } } async function deleteProjectCurrent() { if (!CurrentProjectId) return; if (!confirm('确认删除该项目吗?')) return; const resp = await api('api.php?action=delete_project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: CurrentProjectId }) }); if (resp && resp.ok) { // 删除项目的同时清理保存的AI润色、上次时间、计划日期 try { localStorage.removeItem('ai_last_ts_' + CurrentProjectId); localStorage.removeItem('project_eta_' + CurrentProjectId); } catch (e) {} alert('已删除'); location.href = 'index.php'; } else { alert(resp?.message || '删除失败'); } } 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 loadProject(); } 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 loadProject(); } 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 loadProject(); } 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'); } else { alert(resp?.message || '导出失败'); } } 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 loadProject(); } else { alert(resp?.message || '清理失败'); } } document.addEventListener('DOMContentLoaded', () => { // 退出登录按钮 const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) logoutBtn.addEventListener('click', async () => { try { await api('api.php?action=logout', { method: 'POST' }); } catch (e) {} try { localStorage.removeItem('auth_ok'); } catch (e) {} location.href = 'index.php'; }); // 绑定操作按钮 document.getElementById('saveMetaBtn').addEventListener('click', applyProjectMeta); document.getElementById('deleteProjectBtn').addEventListener('click', deleteProjectCurrent); document.getElementById('exportHtmlBtn').addEventListener('click', exportProjectHtml); document.getElementById('cleanupBtn').addEventListener('click', cleanupUnusedUploads); document.getElementById('addNoteBtn').addEventListener('click', addNote); // 已移除前端 AI Key 输入,改为后端文件配置 // AI润色提示 const aiBtn = document.getElementById('aiEnhanceBtn'); if (aiBtn) { aiBtn.addEventListener('click', async () => { try { const box = document.getElementById('projectRemind'); if (!box) return; const raw = (box.getAttribute('data-raw') || box.textContent || ''); if (!raw) { alert('当前无提醒内容'); return; } aiBtn.disabled = true; aiBtn.textContent = 'AI润色中...'; // 进度提示:结合笔记进行润色 box.textContent = 'AI正在结合笔记进行润色…'; const resp = await api('api.php?action=ai_enrich_reminder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ raw: raw, project: { id: CurrentProjectId, name: CurrentProject?.name, status: CurrentProject?.status }, include_notes: true }) }); if (resp && resp.ok && resp.text) { box.textContent = resp.text; try { localStorage.setItem('ai_last_ts_' + CurrentProjectId, String(Date.now())); } catch (e) {} } else { const msg = resp?.message || 'AI润色失败'; box.textContent = msg; alert(msg); } } catch (e) { alert('AI调用失败'); } finally { aiBtn.disabled = false; aiBtn.textContent = 'AI润色提示'; } }); } // 手动润色:仅在点击“AI润色提示”时触发 // 计划完成日期(本地存储,不影响后端数据) const etaInput = document.getElementById('etaInput'); if (etaInput) { etaInput.addEventListener('change', () => { if (!CurrentProjectId) return; const key = 'project_eta_' + CurrentProjectId; const v = etaInput.value || ''; if (v) localStorage.setItem(key, v); else localStorage.removeItem(key); updateProjectReminder(); }); } // 新增笔记添加图片 const addImgBtn = document.getElementById('newNoteAddImageBtn'); const imgInput = document.getElementById('newNoteImageInput'); const ta = document.getElementById('newNoteInput'); if (addImgBtn && imgInput && ta) { addImgBtn.addEventListener('click', () => imgInput.click()); imgInput.addEventListener('change', async () => { const f = imgInput.files?.[0]; if (!f) return; if (!isImageFile(f)) { alert('请选择图片文件'); return; } const up = await uploadFile(f); if (up && up.ok && up.url) { insertAtCursor(ta, `![image](${up.url})`); } else { alert(up?.message || '上传失败'); } imgInput.value = ''; }); } // 加载项目 loadProject(); });