din/static/app.js
ching c9ab4da3b5 优化 UI 和交互体验
- 按钮改用 SVG 图标,更现代简洁
- 移除按钮文字,纯图标设计
- 添加自定义确认弹窗,替换浏览器默认弹窗
- 删除功能添加确认对话框
- 优化按钮动画和光晕效果
2026-02-21 09:29:13 +00:00

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();