点击记录 · 长按添加备注
+diff --git a/static/app.js b/static/app.js index 0b2d2ee..b69a4f2 100644 --- a/static/app.js +++ b/static/app.js @@ -1,98 +1,124 @@ -const API_BASE = ''; +const API = ''; -// 状态 -let currentRecordId = null; -let lastRecordTime = 0; -let longPressTimer = null; +// ===== STATE ===== +let currentId = null; +let lastClick = 0; +let pressTimer = null; let isLongPress = false; -const LONG_PRESS_DURATION = 500; // 长按触发时间 +const DEBOUNCE = 200; // 200ms 防抖 +const LONG_PRESS = 600; // 600ms 长按 -// DOM 元素 -const triggerArea = document.getElementById('trigger-area'); +// ===== DOM ===== const dinBtn = document.getElementById('din-btn'); -const inputOverlay = document.getElementById('input-overlay'); -const quickInput = document.getElementById('quick-input'); +const floatInput = document.getElementById('float-input'); +const floatField = document.getElementById('float-field'); +const overlay = document.getElementById('overlay'); const toast = document.getElementById('toast'); -const achievementToast = document.getElementById('achievement-toast'); +const confirmOverlay = document.getElementById('confirm-overlay'); +const confirmOk = document.getElementById('confirm-ok'); +const confirmCancel = document.getElementById('confirm-cancel'); -// ========== 核心交互:超快记录 ========== - -// 点击/触摸开始 -dinBtn.addEventListener('touchstart', handleStart, { passive: false }); -dinBtn.addEventListener('mousedown', handleStart); - -// 点击/触摸结束 -dinBtn.addEventListener('touchend', handleEnd, { passive: false }); -dinBtn.addEventListener('mouseup', handleEnd); -dinBtn.addEventListener('mouseleave', cancelLongPress); - -// 防止双击缩放 -dinBtn.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false }); - -function handleStart(e) { - e.preventDefault(); - isLongPress = false; - - // 视觉反馈 - dinBtn.classList.add('recording'); - - // 启动长按计时器 - longPressTimer = setTimeout(() => { - isLongPress = true; - dinBtn.classList.remove('recording'); - // 长按直接进入输入模式 - createRecord(true); - }, LONG_PRESS_DURATION); +// ===== CUSTOM CONFIRM DIALOG ===== +function showConfirm(message, title = '确认删除', icon = '🗑️') { + return new Promise((resolve) => { + document.querySelector('.confirm-title').textContent = title; + document.querySelector('.confirm-message').textContent = message; + document.querySelector('.confirm-icon').textContent = icon; + + confirmOverlay.classList.add('active'); + + const onOk = () => { + confirmOverlay.classList.remove('active'); + cleanup(); + resolve(true); + }; + + const onCancel = () => { + confirmOverlay.classList.remove('active'); + cleanup(); + resolve(false); + }; + + const cleanup = () => { + confirmOk.removeEventListener('click', onOk); + confirmCancel.removeEventListener('click', onCancel); + confirmOverlay.removeEventListener('click', onOverlayClick); + }; + + const onOverlayClick = (e) => { + if (e.target === confirmOverlay) { + onCancel(); + } + }; + + confirmOk.addEventListener('click', onOk); + confirmCancel.addEventListener('click', onCancel); + confirmOverlay.addEventListener('click', onOverlayClick); + }); } -function handleEnd(e) { +// ===== BUTTON INTERACTION ===== + +dinBtn.addEventListener('touchstart', onPress, { passive: false }); +dinBtn.addEventListener('mousedown', onPress); +dinBtn.addEventListener('touchend', onRelease, { passive: false }); +dinBtn.addEventListener('mouseup', onRelease); +dinBtn.addEventListener('mouseleave', onCancel); + +function onPress(e) { e.preventDefault(); - clearTimeout(longPressTimer); + if (Date.now() - lastClick < DEBOUNCE) return; + + isLongPress = false; + dinBtn.classList.add('recording'); + + pressTimer = setTimeout(() => { + isLongPress = true; + dinBtn.classList.remove('recording'); + record(true); // 长按 = 记录 + 输入 + }, LONG_PRESS); +} + +function onRelease(e) { + e.preventDefault(); + clearTimeout(pressTimer); dinBtn.classList.remove('recording'); - // 如果不是长按,直接记录 if (!isLongPress) { - createRecord(false); + lastClick = Date.now(); + record(false); // 短按 = 仅记录 } } -function cancelLongPress() { - clearTimeout(longPressTimer); +function onCancel() { + clearTimeout(pressTimer); dinBtn.classList.remove('recording'); } -// ========== 记录逻辑 ========== +// ===== RECORD ===== -async function createRecord(showInput = false) { - // 防抖:1秒内不能重复点击 - const now = Date.now(); - if (now - lastRecordTime < 1000) return; - lastRecordTime = now; +async function record(openInput = false) { + showToast('已记录!'); try { - // 立即显示反馈(不等待网络) - showToast('已记录!'); - - // 后台发送请求 - const res = await fetch(`${API_BASE}/api/din`, { + const res = await fetch(`${API}/api/din`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: '' }) }); const data = await res.json(); - currentRecordId = data.id; + currentId = data.id; - // 更新统计和列表 - updateStats(); - prependToList(data); + // 并行更新 UI + Promise.all([ + refreshStats(), + addChip(data), + checkAchievements() + ]); - // 检查成就 - checkNewAchievements(); - - // 如果需要输入,显示输入面板 - if (showInput) { - showInputPanel(data.created_at); + if (openInput) { + openFloatInput(data.created_at, false); // false = 新建模式 } } catch (err) { @@ -101,319 +127,290 @@ async function createRecord(showInput = false) { } } -// ========== 输入面板 ========== +// ===== FLOAT INPUT ===== -function showInputPanel(timestamp) { - document.getElementById('input-timestamp').textContent = formatTimeOnly(timestamp); - inputOverlay.classList.add('active'); - quickInput.value = ''; - setTimeout(() => quickInput.focus(), 100); +function openFloatInput(timestamp, isEdit = false) { + document.getElementById('float-time').textContent = formatTime(timestamp); + document.querySelector('.float-title').textContent = isEdit ? '编辑记录' : '添加备注'; + floatField.value = ''; + floatInput.classList.add('active'); + overlay.classList.add('active'); + setTimeout(() => floatField.focus(), 50); } -function hideInputPanel() { - inputOverlay.classList.remove('active'); - quickInput.blur(); +function closeFloatInput() { + floatInput.classList.remove('active'); + overlay.classList.remove('active'); + floatField.blur(); } -// 保存按钮 +// 保存 document.getElementById('btn-save').addEventListener('click', async () => { - if (!currentRecordId) return; - - const content = quickInput.value.trim(); - if (!content) { - hideInputPanel(); + const content = floatField.value.trim(); + if (!content || !currentId) { + closeFloatInput(); return; } + await fetch(`${API}/api/din/${currentId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }) + }); + + updateChip(currentId, content); + closeFloatInput(); + showToast('已保存'); +}); + +// 删除 +document.getElementById('btn-delete').addEventListener('click', async () => { + if (!currentId) { + console.log('No currentId to delete'); + closeFloatInput(); + return; + } + + const confirmed = await showConfirm('确定要删除这条记录吗?此操作不可恢复。'); + if (!confirmed) return; + try { - await fetch(`${API_BASE}/api/din/${currentRecordId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content }) - }); + const res = await fetch(`${API}/api/din/${currentId}`, { method: 'DELETE' }); - // 更新列表中的内容 - updateListItem(currentRecordId, content); - hideInputPanel(); - showToast('已保存'); + if (!res.ok) { + throw new Error('Delete failed'); + } + // 移除 chip - 使用字符串比较 + const chip = document.querySelector(`.recent-chip[data-id="${currentId}"]`); + console.log('Looking for chip with id:', currentId, 'found:', chip); + if (chip) chip.remove(); + + await refreshStats(); + closeFloatInput(); + showToast('已删除'); + currentId = null; // 清除当前ID } catch (err) { - showToast('保存失败', false); + console.error('Delete error:', err); + showToast('删除失败', false); } }); -// 跳过按钮 -document.getElementById('btn-skip').addEventListener('click', () => { - hideInputPanel(); -}); +// 跳过 +document.getElementById('btn-skip').addEventListener('click', closeFloatInput); // 点击遮罩关闭 -inputOverlay.addEventListener('click', (e) => { - if (e.target === inputOverlay) { - hideInputPanel(); - } -}); +overlay.addEventListener('click', closeFloatInput); // 回车保存 -quickInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - document.getElementById('btn-save').click(); - } +floatField.addEventListener('keypress', e => { + if (e.key === 'Enter') document.getElementById('btn-save').click(); }); -// 快速标签 +// 快捷标签 document.querySelectorAll('.tag').forEach(tag => { tag.addEventListener('click', () => { - const text = tag.dataset.text; - quickInput.value = text; + floatField.value = tag.dataset.text; document.getElementById('btn-save').click(); }); }); -// ========== 数据更新 ========== +// ===== CHIPS ===== -async function updateStats() { - try { - const res = await fetch(`${API_BASE}/api/stats`); - const stats = await res.json(); - - document.getElementById('stat-today').textContent = stats.today; - document.getElementById('stat-week').textContent = stats.week; - document.getElementById('stat-total').textContent = stats.total; - document.getElementById('recent-count').textContent = stats.total; - } catch (err) { - console.error('更新统计失败:', err); - } -} - -function prependToList(record) { - const list = document.getElementById('recent-list'); - const emptyMsg = list.querySelector('.recent-item .empty'); - if (emptyMsg) { - list.innerHTML = ''; - } +function addChip(record) { + const container = document.getElementById('recent-chips'); + const empty = container.querySelector('.empty'); + if (empty) empty.remove(); - const item = document.createElement('div'); - item.className = 'recent-item'; - item.dataset.id = record.id; - item.innerHTML = ` - ${formatTimeOnly(record.created_at)} - (未备注) + const chip = document.createElement('div'); + chip.className = 'recent-chip'; + chip.dataset.id = record.id; + chip.innerHTML = ` + ${formatTime(record.created_at)} + 新记录 `; - list.insertBefore(item, list.firstChild); + chip.addEventListener('click', () => { + currentId = String(record.id); // 确保是字符串 + console.log('Chip clicked, currentId set to:', currentId); + openFloatInput(record.created_at, true); // true = 编辑模式 + }); - // 保持最多10条 - while (list.children.length > 10) { - list.removeChild(list.lastChild); + container.insertBefore(chip, container.firstChild); + + // 保持最多 10 个 + while (container.children.length > 10) { + container.removeChild(container.lastChild); } } -function updateListItem(id, content) { - const item = document.querySelector(`.recent-item[data-id="${id}"]`); - if (item) { - const contentEl = item.querySelector('.recent-content'); - contentEl.textContent = content; - contentEl.classList.remove('empty'); +function updateChip(id, content) { + const chip = document.querySelector(`.recent-chip[data-id="${id}"]`); + if (chip) { + chip.querySelector('span:last-child').textContent = content; } } -// ========== 成就检查 ========== +// ===== STATS ===== -let lastAchievementCount = 0; - -async function checkNewAchievements() { +async function refreshStats() { try { - const res = await fetch(`${API_BASE}/api/achievements`); + const res = await fetch(`${API}/api/stats`); + const s = await res.json(); + + document.getElementById('stat-today').textContent = s.today; + document.getElementById('stat-week').textContent = s.week; + document.getElementById('stat-month').textContent = s.month; + document.getElementById('stat-total').textContent = s.total; + + // 增长率 + updateGrowth('growth-today', s.day_growth); + updateGrowth('growth-week', s.week_growth); + updateGrowth('growth-month', s.month_growth); + } catch (err) { + console.error(err); + } +} + +function updateGrowth(id, value) { + const el = document.getElementById(id); + if (!el) return; + if (value > 0) { + el.textContent = `+${value}%`; + el.classList.remove('negative'); + } else if (value < 0) { + el.textContent = `${value}%`; + el.classList.add('negative'); + } else { + el.textContent = '-'; + } +} + +// ===== ACHIEVEMENTS ===== + +async function checkAchievements() { + try { + const res = await fetch(`${API}/api/achievements`); const data = await res.json(); - if (data.unlocked_count > lastAchievementCount) { - // 有新成就解锁 - const newAchievements = data.achievements.filter(a => a.unlocked).slice(-1); - if (newAchievements.length > 0) { - showAchievementToast(newAchievements[0]); - } - } - lastAchievementCount = data.unlocked_count; - } catch (err) { - console.error('检查成就失败:', err); - } -} - -function showAchievementToast(achievement) { - const icon = achievementToast.querySelector('.achievement-icon'); - const text = document.getElementById('achievement-text'); - - icon.textContent = achievement.icon; - text.textContent = `解锁:${achievement.name}`; - - achievementToast.classList.add('show'); - - setTimeout(() => { - achievementToast.classList.remove('show'); - }, 3000); -} - -// ========== 辅助函数 ========== - -function showToast(message, success = true) { - toast.textContent = message; - toast.style.background = success ? '#ff4757' : '#ff6b6b'; - toast.classList.add('show'); - - setTimeout(() => { - toast.classList.remove('show'); - }, 1500); -} - -function formatTimeOnly(isoString) { - const date = new Date(isoString); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - return `${hours}:${minutes}`; -} - -// ========== 初始化 ========== - -async function init() { - await updateStats(); - await loadRecent(); - await checkNewAchievements(); -} - -async function loadRecent() { - try { - const res = await fetch(`${API_BASE}/api/din?limit=10`); - const records = await res.json(); + document.getElementById('ach-progress').textContent = + `${data.unlocked_count}/${data.total_count}`; - const list = document.getElementById('recent-list'); - document.getElementById('recent-count').textContent = records.length; - - if (records.length === 0) { - list.innerHTML = '
点击任意处记录 · 长按快捷输入
+Do It Now - 想到就做
+点击记录 · 长按添加备注
+