- 全屏点击即可记录,响应更快 - 长按直接进入输入模式 - 快速标签:工作/学习/运动/休息/创意/其他 - 底部常驻统计,无需切换页面 - 右侧面板显示最近记录 - 成就解锁动画提示 - 优化移动端触摸体验
310 lines
8.7 KiB
JavaScript
310 lines
8.7 KiB
JavaScript
const API_BASE = '';
|
||
|
||
// 状态
|
||
let currentRecordId = null;
|
||
let lastRecordTime = 0;
|
||
let longPressTimer = null;
|
||
let isLongPress = false;
|
||
const LONG_PRESS_DURATION = 500; // 长按触发时间
|
||
|
||
// 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;
|
||
|
||
// 视觉反馈
|
||
dinBtn.classList.add('recording');
|
||
|
||
// 启动长按计时器
|
||
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 {
|
||
// 立即显示反馈(不等待网络)
|
||
showToast('已记录!');
|
||
|
||
// 后台发送请求
|
||
const res = await fetch(`${API_BASE}/api/din`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ content: '' })
|
||
});
|
||
|
||
const data = await res.json();
|
||
currentRecordId = data.id;
|
||
|
||
// 更新统计和列表
|
||
updateStats();
|
||
prependToList(data);
|
||
|
||
// 检查成就
|
||
checkNewAchievements();
|
||
|
||
// 如果需要输入,显示输入面板
|
||
if (showInput) {
|
||
showInputPanel(data.created_at);
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('记录失败:', err);
|
||
showToast('记录失败', false);
|
||
}
|
||
}
|
||
|
||
// ========== 输入面板 ==========
|
||
|
||
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 = quickInput.value.trim();
|
||
if (!content) {
|
||
hideInputPanel();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await fetch(`${API_BASE}/api/din/${currentRecordId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ content })
|
||
});
|
||
|
||
// 更新列表中的内容
|
||
updateListItem(currentRecordId, content);
|
||
hideInputPanel();
|
||
showToast('已保存');
|
||
|
||
} catch (err) {
|
||
showToast('保存失败', false);
|
||
}
|
||
});
|
||
|
||
// 跳过按钮
|
||
document.getElementById('btn-skip').addEventListener('click', () => {
|
||
hideInputPanel();
|
||
});
|
||
|
||
// 点击遮罩关闭
|
||
inputOverlay.addEventListener('click', (e) => {
|
||
if (e.target === inputOverlay) {
|
||
hideInputPanel();
|
||
}
|
||
});
|
||
|
||
// 回车保存
|
||
quickInput.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;
|
||
document.getElementById('btn-save').click();
|
||
});
|
||
});
|
||
|
||
// ========== 数据更新 ==========
|
||
|
||
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 = '';
|
||
}
|
||
|
||
const item = document.createElement('div');
|
||
item.className = 'recent-item';
|
||
item.dataset.id = record.id;
|
||
item.innerHTML = `
|
||
<span class="recent-time">${formatTimeOnly(record.created_at)}</span>
|
||
<span class="recent-content empty">(未备注)</span>
|
||
`;
|
||
|
||
list.insertBefore(item, list.firstChild);
|
||
|
||
// 保持最多10条
|
||
while (list.children.length > 10) {
|
||
list.removeChild(list.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');
|
||
}
|
||
}
|
||
|
||
// ========== 成就检查 ==========
|
||
|
||
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');
|
||
|
||
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();
|
||
|
||
const list = document.getElementById('recent-list');
|
||
document.getElementById('recent-count').textContent = records.length;
|
||
|
||
if (records.length === 0) {
|
||
list.innerHTML = '<div class="recent-item"><span class="recent-content empty">点击大按钮开始记录</span></div>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = records.map(r => `
|
||
<div class="recent-item" data-id="${r.id}">
|
||
<span class="recent-time">${formatTimeOnly(r.created_at)}</span>
|
||
<span class="recent-content ${!r.content ? 'empty' : ''}">${r.content || '(未备注)'}</span>
|
||
</div>
|
||
`).join('');
|
||
} catch (err) {
|
||
console.error('加载记录失败:', err);
|
||
}
|
||
}
|
||
|
||
// 启动
|
||
init();
|