diff --git a/static/app.js b/static/app.js index 00af795..70eb3c0 100644 --- a/static/app.js +++ b/static/app.js @@ -1,256 +1,129 @@ -// API 基础地址 const API_BASE = ''; -// 离线队列(用于后台同步) -const DB_NAME = 'din-offline'; -const DB_VERSION = 1; -let db = null; - -// 初始化 IndexedDB -function initDB() { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onerror = () => reject(request.error); - request.onsuccess = () => { - db = request.result; - resolve(db); - }; - request.onupgradeneeded = (e) => { - const db = e.target.result; - if (!db.objectStoreNames.contains('pending')) { - db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true }); - } - }; - }); -} - -// 添加到离线队列 -async function addToQueue(data) { - if (!db) await initDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction('pending', 'readwrite'); - const store = tx.objectStore('pending'); - const request = store.add({ data, timestamp: Date.now() }); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); -} - -// 注册后台同步 -async function registerSync() { - if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) { - const reg = await navigator.serviceWorker.ready; - await reg.sync.register('sync-din'); - } -} - -// DOM 元素 -const dinBtn = document.getElementById('din-btn'); -const inputSection = document.getElementById('input-section'); -const dinContent = document.getElementById('din-content'); -const saveBtn = document.getElementById('save-btn'); -const skipBtn = document.getElementById('skip-btn'); -const toast = document.getElementById('toast'); - // 状态 let currentRecordId = null; +let lastRecordTime = 0; +let longPressTimer = null; +let isLongPress = false; +const LONG_PRESS_DURATION = 500; // 长按触发时间 -// 初始化 -async function init() { - await initDB(); - await loadStats(); - await loadRecent(); - await loadAchievements(); +// DOM 元素 +const triggerArea = document.getElementById('trigger-area'); +const dinBtn = document.getElementById('din-btn'); +const inputOverlay = document.getElementById('input-overlay'); +const quickInput = document.getElementById('quick-input'); +const toast = document.getElementById('toast'); +const achievementToast = document.getElementById('achievement-toast'); + +// ========== 核心交互:超快记录 ========== + +// 点击/触摸开始 +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; - // 检查网络状态 - window.addEventListener('online', () => { - showToast('已连接到网络', true); - syncPendingData(); - }); - window.addEventListener('offline', () => { - showToast('进入离线模式', false); - }); -} - -// 同步离线数据 -async function syncPendingData() { - if (!db || !navigator.onLine) return; - - return new Promise((resolve, reject) => { - const tx = db.transaction('pending', 'readonly'); - const store = tx.objectStore('pending'); - const request = store.getAll(); - - request.onsuccess = async () => { - const pending = request.result; - if (pending.length === 0) { - resolve(); - return; - } - - for (const item of pending) { - try { - await fetch(`${API_BASE}/api/din`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item.data) - }); - - // 删除已同步的 - const delTx = db.transaction('pending', 'readwrite'); - const delStore = delTx.objectStore('pending'); - await delStore.delete(item.id); - } catch (err) { - console.error('Sync failed for item:', item.id); - } - } - - await loadStats(); - await loadRecent(); - resolve(); - }; - - request.onerror = () => reject(request.error); - }); -} - -// 加载统计数据 -async function loadStats() { - 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-month').textContent = stats.month; - document.getElementById('stat-total').textContent = stats.total; - - // 增长率 - updateGrowth('growth-today', stats.day_growth); - updateGrowth('growth-week', stats.week_growth); - updateGrowth('growth-month', stats.month_growth); - } catch (err) { - console.error('加载统计失败:', err); - } -} - -function updateGrowth(id, value) { - const el = document.getElementById(id); - if (value > 0) { - el.textContent = `+${value}%`; - el.classList.remove('negative'); - } else if (value < 0) { - el.textContent = `${value}%`; - el.classList.add('negative'); - } else { - el.textContent = '-'; - } -} - -// 加载最近记录 -async function loadRecent() { - try { - const res = await fetch(`${API_BASE}/api/din?limit=10`); - const records = await res.json(); - - const list = document.getElementById('recent-list'); - - if (records.length === 0) { - list.innerHTML = '
还没有记录,点击上方按钮开始!
'; - return; - } - - list.innerHTML = records.map(r => ` -
-
${formatTime(r.created_at)}
-
${r.content || '(无描述)'}
-
- - -
-
- `).join(''); - } catch (err) { - console.error('加载记录失败:', err); - } -} - -// 加载成就 -async function loadAchievements() { - try { - const res = await fetch(`${API_BASE}/api/achievements`); - const data = await res.json(); - - document.getElementById('achievement-progress').textContent = - `(${data.unlocked_count}/${data.total_count})`; - - const list = document.getElementById('achievement-list'); - list.innerHTML = data.achievements.map(a => ` -
-
${a.icon}
-
${a.name}
-
${a.desc}
-
- `).join(''); - } catch (err) { - console.error('加载成就失败:', err); - } -} - -// 点击大按钮 -dinBtn.addEventListener('click', async () => { + // 视觉反馈 dinBtn.classList.add('recording'); - dinBtn.disabled = true; - const content = ''; // 先创建空记录 + // 启动长按计时器 + longPressTimer = setTimeout(() => { + isLongPress = true; + dinBtn.classList.remove('recording'); + // 长按直接进入输入模式 + createRecord(true); + }, LONG_PRESS_DURATION); +} + +function handleEnd(e) { + e.preventDefault(); + clearTimeout(longPressTimer); + dinBtn.classList.remove('recording'); + + // 如果不是长按,直接记录 + if (!isLongPress) { + createRecord(false); + } +} + +function cancelLongPress() { + clearTimeout(longPressTimer); + dinBtn.classList.remove('recording'); +} + +// ========== 记录逻辑 ========== + +async function createRecord(showInput = false) { + // 防抖:1秒内不能重复点击 + const now = Date.now(); + if (now - lastRecordTime < 1000) return; + lastRecordTime = now; try { - // 检查网络状态 - if (!navigator.onLine) { - // 离线模式:存入队列 - await addToQueue({ content }); - await registerSync(); - showToast('已保存,将在联网时同步'); - - // 显示输入区域(离线编辑) - currentRecordId = 'offline_' + Date.now(); - inputSection.classList.remove('hidden'); - dinContent.value = ''; - dinContent.focus(); - return; - } + // 立即显示反馈(不等待网络) + showToast('已记录!'); + // 后台发送请求 const res = await fetch(`${API_BASE}/api/din`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content }) + body: JSON.stringify({ content: '' }) }); const data = await res.json(); currentRecordId = data.id; - // 显示输入区域 - inputSection.classList.remove('hidden'); - dinContent.value = ''; - dinContent.focus(); + // 更新统计和列表 + updateStats(); + prependToList(data); - // 更新统计 - await loadStats(); + // 检查成就 + checkNewAchievements(); + + // 如果需要输入,显示输入面板 + if (showInput) { + showInputPanel(data.created_at); + } } catch (err) { - console.error('创建记录失败:', err); - showToast('记录失败,请重试', false); - } finally { - dinBtn.classList.remove('recording'); - dinBtn.disabled = false; + console.error('记录失败:', err); + showToast('记录失败', false); } -}); +} -// 保存内容 -saveBtn.addEventListener('click', async () => { +// ========== 输入面板 ========== + +function showInputPanel(timestamp) { + document.getElementById('input-timestamp').textContent = formatTimeOnly(timestamp); + inputOverlay.classList.add('active'); + quickInput.value = ''; + setTimeout(() => quickInput.focus(), 100); +} + +function hideInputPanel() { + inputOverlay.classList.remove('active'); + quickInput.blur(); +} + +// 保存按钮 +document.getElementById('btn-save').addEventListener('click', async () => { if (!currentRecordId) return; - const content = dinContent.value.trim(); + const content = quickInput.value.trim(); + if (!content) { + hideInputPanel(); + return; + } try { await fetch(`${API_BASE}/api/din/${currentRecordId}`, { @@ -259,129 +132,177 @@ saveBtn.addEventListener('click', async () => { body: JSON.stringify({ content }) }); - inputSection.classList.add('hidden'); - currentRecordId = null; - - await loadRecent(); - await loadAchievements(); - showToast('记录成功!'); + // 更新列表中的内容 + updateListItem(currentRecordId, content); + hideInputPanel(); + showToast('已保存'); } catch (err) { - console.error('保存失败:', err); showToast('保存失败', false); } }); -// 跳过 -skipBtn.addEventListener('click', async () => { - inputSection.classList.add('hidden'); - currentRecordId = null; - await loadRecent(); - await loadAchievements(); - showToast('记录成功!'); +// 跳过按钮 +document.getElementById('btn-skip').addEventListener('click', () => { + hideInputPanel(); +}); + +// 点击遮罩关闭 +inputOverlay.addEventListener('click', (e) => { + if (e.target === inputOverlay) { + hideInputPanel(); + } }); // 回车保存 -dinContent.addEventListener('keypress', (e) => { +quickInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { - saveBtn.click(); + document.getElementById('btn-save').click(); } }); -// 点击外部自动跳过(不丢失记录) -document.addEventListener('click', (e) => { - // 输入框隐藏时不处理 - if (inputSection.classList.contains('hidden')) return; - - // 点击输入框内部不处理 - if (inputSection.contains(e.target)) return; - - // 点击大按钮时不处理(刚点击过) - if (dinBtn.contains(e.target)) return; - - // 点击其他地方:自动跳过 - skipBtn.click(); +// 快速标签 +document.querySelectorAll('.tag').forEach(tag => { + tag.addEventListener('click', () => { + const text = tag.dataset.text; + quickInput.value = text; + document.getElementById('btn-save').click(); + }); }); -// 编辑记录 -async function editRecord(id, currentContent) { - const newContent = prompt('修改内容:', currentContent); - if (newContent === null) return; // 取消 - +// ========== 数据更新 ========== + +async function updateStats() { try { - await fetch(`${API_BASE}/api/din/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content: newContent.trim() }) - }); + const res = await fetch(`${API_BASE}/api/stats`); + const stats = await res.json(); - await loadRecent(); - showToast('修改成功!'); + 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); - showToast('修改失败', false); + console.error('更新统计失败:', err); } } -// 删除记录 -async function deleteRecord(id) { - if (!confirm('确定删除这条记录?')) return; +function prependToList(record) { + const list = document.getElementById('recent-list'); + const emptyMsg = list.querySelector('.recent-item .empty'); + if (emptyMsg) { + list.innerHTML = ''; + } - try { - await fetch(`${API_BASE}/api/din/${id}`, { method: 'DELETE' }); - - await loadRecent(); - await loadStats(); - await loadAchievements(); - showToast('删除成功'); - } catch (err) { - console.error('删除失败:', err); - showToast('删除失败', false); + const item = document.createElement('div'); + item.className = 'recent-item'; + item.dataset.id = record.id; + item.innerHTML = ` + ${formatTimeOnly(record.created_at)} + (未备注) + `; + + list.insertBefore(item, list.firstChild); + + // 保持最多10条 + while (list.children.length > 10) { + list.removeChild(list.lastChild); } } -// 显示提示 -function showToast(text, success = true) { - toast.querySelector('.toast-text').textContent = text; - toast.querySelector('.toast-icon').textContent = success ? '✓' : '✗'; - toast.style.background = success ? 'rgba(74,222,128,0.9)' : 'rgba(248,113,113,0.9)'; +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'); + } +} + +// ========== 成就检查 ========== + +let lastAchievementCount = 0; + +async function checkNewAchievements() { + try { + const res = await fetch(`${API_BASE}/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'); - toast.classList.remove('hidden'); + icon.textContent = achievement.icon; + text.textContent = `解锁:${achievement.name}`; + + achievementToast.classList.add('show'); setTimeout(() => { - toast.classList.add('hidden'); - }, 2000); + achievementToast.classList.remove('show'); + }, 3000); } -// 格式化时间 -function formatTime(isoString) { - const date = new Date(isoString); - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const recordDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); +// ========== 辅助函数 ========== + +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'); - - if (recordDate.getTime() === today.getTime()) { - return `${hours}:${minutes}`; - } - - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); - - if (recordDate.getTime() === yesterday.getTime()) { - return `昨天 ${hours}:${minutes}`; - } - - return `${date.getMonth() + 1}/${date.getDate()}`; + return `${hours}:${minutes}`; } -// HTML 转义 -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; +// ========== 初始化 ========== + +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(); + + const list = document.getElementById('recent-list'); + document.getElementById('recent-count').textContent = records.length; + + if (records.length === 0) { + list.innerHTML = '
点击大按钮开始记录
'; + return; + } + + list.innerHTML = records.map(r => ` +
+ ${formatTimeOnly(r.created_at)} + ${r.content || '(未备注)'} +
+ `).join(''); + } catch (err) { + console.error('加载记录失败:', err); + } } // 启动 diff --git a/static/index.html b/static/index.html index 6ee1c37..117899a 100644 --- a/static/index.html +++ b/static/index.html @@ -2,8 +2,8 @@ - - + + @@ -12,83 +12,83 @@ -
- -
-

🦐 din

-

Do It Now - 想到就做

-
- - -
-
-
0
-
今日
-
-
-
-
-
0
-
本周
-
-
-
-
-
0
-
本月
-
-
-
-
-
0
-
总计
-
-
- - -
- -
- - - - - -
-

📝 最近记录

-
-
还没有记录,点击上方按钮开始!
-
-
- - -
-

🏆 成就 (0/8)

-
- -
-
+ +
+ +

点击任意处记录 · 长按快捷输入

- -