- 按钮改用 SVG 图标,更现代简洁 - 移除按钮文字,纯图标设计 - 添加自定义确认弹窗,替换浏览器默认弹窗 - 删除功能添加确认对话框 - 优化按钮动画和光晕效果
417 lines
12 KiB
JavaScript
417 lines
12 KiB
JavaScript
const API = '';
|
|
|
|
// ===== STATE =====
|
|
let currentId = null;
|
|
let lastClick = 0;
|
|
let pressTimer = null;
|
|
let isLongPress = false;
|
|
const DEBOUNCE = 200; // 200ms 防抖
|
|
const LONG_PRESS = 600; // 600ms 长按
|
|
|
|
// ===== DOM =====
|
|
const dinBtn = document.getElementById('din-btn');
|
|
const floatInput = document.getElementById('float-input');
|
|
const floatField = document.getElementById('float-field');
|
|
const overlay = document.getElementById('overlay');
|
|
const toast = document.getElementById('toast');
|
|
const confirmOverlay = document.getElementById('confirm-overlay');
|
|
const confirmOk = document.getElementById('confirm-ok');
|
|
const confirmCancel = document.getElementById('confirm-cancel');
|
|
|
|
// ===== 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);
|
|
});
|
|
}
|
|
|
|
// ===== 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();
|
|
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) {
|
|
lastClick = Date.now();
|
|
record(false); // 短按 = 仅记录
|
|
}
|
|
}
|
|
|
|
function onCancel() {
|
|
clearTimeout(pressTimer);
|
|
dinBtn.classList.remove('recording');
|
|
}
|
|
|
|
// ===== RECORD =====
|
|
|
|
async function record(openInput = false) {
|
|
showToast('已记录!');
|
|
|
|
try {
|
|
const res = await fetch(`${API}/api/din`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content: '' })
|
|
});
|
|
|
|
const data = await res.json();
|
|
currentId = data.id;
|
|
|
|
// 并行更新 UI
|
|
Promise.all([
|
|
refreshStats(),
|
|
addChip(data),
|
|
checkAchievements()
|
|
]);
|
|
|
|
if (openInput) {
|
|
openFloatInput(data.created_at, false); // false = 新建模式
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('记录失败:', err);
|
|
showToast('记录失败', false);
|
|
}
|
|
}
|
|
|
|
// ===== FLOAT INPUT =====
|
|
|
|
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 closeFloatInput() {
|
|
floatInput.classList.remove('active');
|
|
overlay.classList.remove('active');
|
|
floatField.blur();
|
|
}
|
|
|
|
// 保存
|
|
document.getElementById('btn-save').addEventListener('click', async () => {
|
|
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 {
|
|
const res = await fetch(`${API}/api/din/${currentId}`, { method: 'DELETE' });
|
|
|
|
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) {
|
|
console.error('Delete error:', err);
|
|
showToast('删除失败', false);
|
|
}
|
|
});
|
|
|
|
// 跳过
|
|
document.getElementById('btn-skip').addEventListener('click', closeFloatInput);
|
|
|
|
// 点击遮罩关闭
|
|
overlay.addEventListener('click', closeFloatInput);
|
|
|
|
// 回车保存
|
|
floatField.addEventListener('keypress', e => {
|
|
if (e.key === 'Enter') document.getElementById('btn-save').click();
|
|
});
|
|
|
|
// 快捷标签
|
|
document.querySelectorAll('.tag').forEach(tag => {
|
|
tag.addEventListener('click', () => {
|
|
floatField.value = tag.dataset.text;
|
|
document.getElementById('btn-save').click();
|
|
});
|
|
});
|
|
|
|
// ===== CHIPS =====
|
|
|
|
function addChip(record) {
|
|
const container = document.getElementById('recent-chips');
|
|
const empty = container.querySelector('.empty');
|
|
if (empty) empty.remove();
|
|
|
|
const chip = document.createElement('div');
|
|
chip.className = 'recent-chip';
|
|
chip.dataset.id = record.id;
|
|
chip.innerHTML = `
|
|
<span class="time">${formatTime(record.created_at)}</span>
|
|
<span>新记录</span>
|
|
`;
|
|
|
|
chip.addEventListener('click', () => {
|
|
currentId = String(record.id); // 确保是字符串
|
|
console.log('Chip clicked, currentId set to:', currentId);
|
|
openFloatInput(record.created_at, true); // true = 编辑模式
|
|
});
|
|
|
|
container.insertBefore(chip, container.firstChild);
|
|
|
|
// 保持最多 10 个
|
|
while (container.children.length > 10) {
|
|
container.removeChild(container.lastChild);
|
|
}
|
|
}
|
|
|
|
function updateChip(id, content) {
|
|
const chip = document.querySelector(`.recent-chip[data-id="${id}"]`);
|
|
if (chip) {
|
|
chip.querySelector('span:last-child').textContent = content;
|
|
}
|
|
}
|
|
|
|
// ===== STATS =====
|
|
|
|
async function refreshStats() {
|
|
try {
|
|
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();
|
|
|
|
document.getElementById('ach-progress').textContent =
|
|
`${data.unlocked_count}/${data.total_count}`;
|
|
|
|
const list = document.getElementById('achievements-list');
|
|
list.innerHTML = data.achievements.map(a => `
|
|
<div class="achievement-badge ${a.unlocked ? 'unlocked' : ''}" title="${a.desc}">
|
|
<span class="achievement-icon">${a.icon}</span>
|
|
<span class="achievement-name">${a.name}</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// ===== HISTORY PANEL =====
|
|
|
|
const historyPanel = document.getElementById('history-panel');
|
|
const historyList = document.getElementById('history-list');
|
|
|
|
document.getElementById('btn-view-all').addEventListener('click', () => {
|
|
historyPanel.classList.add('active');
|
|
loadHistory();
|
|
});
|
|
|
|
document.getElementById('history-close').addEventListener('click', () => {
|
|
historyPanel.classList.remove('active');
|
|
});
|
|
|
|
async function loadHistory() {
|
|
try {
|
|
const res = await fetch(`${API}/api/din?limit=100`);
|
|
const records = await res.json();
|
|
|
|
if (records.length === 0) {
|
|
historyList.innerHTML = '<div style="text-align:center;color:#555;padding:40px;">暂无记录</div>';
|
|
return;
|
|
}
|
|
|
|
historyList.innerHTML = records.map(r => `
|
|
<div class="history-item" data-id="${r.id}">
|
|
<span class="history-time">${formatTime(r.created_at)}</span>
|
|
<span class="history-content ${!r.content ? 'empty' : ''}">${r.content || '(未备注)'}</span>
|
|
<div class="history-actions">
|
|
<button onclick="editRecord(${r.id}, '${esc(r.content||'')}')" title="编辑">✏️</button>
|
|
<button class="btn-delete" onclick="deleteRecord(${r.id})" title="删除">🗑️</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
window.editRecord = async (id, current) => {
|
|
const content = prompt('修改备注:', current);
|
|
if (content === null) return;
|
|
|
|
await fetch(`${API}/api/din/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content: content.trim() })
|
|
});
|
|
|
|
loadHistory();
|
|
updateChip(id, content);
|
|
showToast('已更新');
|
|
};
|
|
|
|
window.deleteRecord = async (id) => {
|
|
const confirmed = await showConfirm('确定要删除这条记录吗?此操作不可恢复。');
|
|
if (!confirmed) return;
|
|
|
|
await fetch(`${API}/api/din/${id}`, { method: 'DELETE' });
|
|
|
|
loadHistory();
|
|
refreshStats();
|
|
|
|
const chip = document.querySelector(`.recent-chip[data-id="${id}"]`);
|
|
if (chip) chip.remove();
|
|
|
|
showToast('已删除');
|
|
};
|
|
|
|
// ===== UTILS =====
|
|
|
|
function showToast(msg, success = true) {
|
|
toast.textContent = msg;
|
|
toast.style.background = success ? '#ff6b6b' : '#ff5252';
|
|
toast.classList.add('show');
|
|
setTimeout(() => toast.classList.remove('show'), 1500);
|
|
}
|
|
|
|
function formatTime(iso) {
|
|
const d = new Date(iso);
|
|
return `${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}`;
|
|
}
|
|
|
|
function esc(str) {
|
|
return str.replace(/'/g, "\\'").replace(/"/g, '\\"');
|
|
}
|
|
|
|
// ===== INIT =====
|
|
|
|
async function init() {
|
|
await refreshStats();
|
|
await checkAchievements();
|
|
|
|
// 加载最近记录
|
|
try {
|
|
const res = await fetch(`${API}/api/din?limit=10`);
|
|
const records = await res.json();
|
|
records.reverse().forEach(r => addChip(r));
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
init();
|