优化 UI 和交互体验
- 按钮改用 SVG 图标,更现代简洁 - 移除按钮文字,纯图标设计 - 添加自定义确认弹窗,替换浏览器默认弹窗 - 删除功能添加确认对话框 - 优化按钮动画和光晕效果
This commit is contained in:
parent
b4cd0df014
commit
c9ab4da3b5
597
static/app.js
597
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 = `
|
||||
<span class="recent-time">${formatTimeOnly(record.created_at)}</span>
|
||||
<span class="recent-content empty">(未备注)</span>
|
||||
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>
|
||||
`;
|
||||
|
||||
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 = '<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>
|
||||
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);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 历史记录面板 ==========
|
||||
// ===== HISTORY PANEL =====
|
||||
|
||||
const historyOverlay = document.getElementById('history-overlay');
|
||||
const historyPanel = document.getElementById('history-panel');
|
||||
const historyList = document.getElementById('history-list');
|
||||
|
||||
// 打开历史面板
|
||||
document.getElementById('view-all').addEventListener('click', () => {
|
||||
historyOverlay.classList.add('active');
|
||||
document.getElementById('btn-view-all').addEventListener('click', () => {
|
||||
historyPanel.classList.add('active');
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
// 关闭历史面板
|
||||
document.getElementById('close-history').addEventListener('click', () => {
|
||||
historyOverlay.classList.remove('active');
|
||||
});
|
||||
|
||||
// 点击遮罩关闭
|
||||
historyOverlay.addEventListener('click', (e) => {
|
||||
if (e.target === historyOverlay) {
|
||||
historyOverlay.classList.remove('active');
|
||||
}
|
||||
document.getElementById('history-close').addEventListener('click', () => {
|
||||
historyPanel.classList.remove('active');
|
||||
});
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/din?limit=100`);
|
||||
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:#666;padding:40px;">暂无记录</div>';
|
||||
historyList.innerHTML = '<div style="text-align:center;color:#555;padding:40px;">暂无记录</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 按日期分组
|
||||
const grouped = groupByDate(records);
|
||||
|
||||
historyList.innerHTML = Object.entries(grouped).map(([date, items]) => `
|
||||
<div class="history-group">
|
||||
<div class="history-date-header">${date}</div>
|
||||
${items.map(r => `
|
||||
<div class="history-item" data-id="${r.id}">
|
||||
<span class="history-time">${formatTimeOnly(r.created_at)}</span>
|
||||
<span class="history-content ${!r.content ? 'empty' : ''}">${r.content || '(未备注)'}</span>
|
||||
<div class="history-actions">
|
||||
<button onclick="editHistoryItem(${r.id}, '${escapeHtml(r.content || '')}')" title="编辑">✏️</button>
|
||||
<button class="btn-delete" onclick="deleteHistoryItem(${r.id})" title="删除">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
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);
|
||||
showToast('加载失败', false);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function groupByDate(records) {
|
||||
const groups = {};
|
||||
records.forEach(r => {
|
||||
const date = new Date(r.created_at);
|
||||
const key = `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(r);
|
||||
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() })
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
// 编辑历史记录
|
||||
window.editHistoryItem = async function(id, currentContent) {
|
||||
const newContent = prompt('修改备注:', currentContent);
|
||||
if (newContent === null) return;
|
||||
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/din/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: newContent.trim() })
|
||||
});
|
||||
|
||||
showToast('已更新');
|
||||
loadHistory(); // 刷新历史列表
|
||||
loadRecent(); // 刷新最近列表
|
||||
} catch (err) {
|
||||
showToast('更新失败', false);
|
||||
}
|
||||
loadHistory();
|
||||
updateChip(id, content);
|
||||
showToast('已更新');
|
||||
};
|
||||
|
||||
// 删除历史记录
|
||||
window.deleteHistoryItem = async function(id) {
|
||||
if (!confirm('确定删除这条记录?')) return;
|
||||
window.deleteRecord = async (id) => {
|
||||
const confirmed = await showConfirm('确定要删除这条记录吗?此操作不可恢复。');
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/din/${id}`, { method: 'DELETE' });
|
||||
|
||||
showToast('已删除');
|
||||
loadHistory(); // 刷新历史列表
|
||||
loadRecent(); // 刷新最近列表
|
||||
updateStats(); // 刷新统计
|
||||
} catch (err) {
|
||||
showToast('删除失败', false);
|
||||
}
|
||||
await fetch(`${API}/api/din/${id}`, { method: 'DELETE' });
|
||||
|
||||
loadHistory();
|
||||
refreshStats();
|
||||
|
||||
const chip = document.querySelector(`.recent-chip[data-id="${id}"]`);
|
||||
if (chip) chip.remove();
|
||||
|
||||
showToast('已删除');
|
||||
};
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML.replace(/'/g, "'").replace(/"/g, """);
|
||||
// ===== 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();
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0d0d0f">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="din">
|
||||
@ -18,90 +18,135 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 主触发区 - 全屏点击即可记录 -->
|
||||
<div class="din-trigger" id="trigger-area">
|
||||
<button class="big-btn" id="din-btn" aria-label="记录 din">
|
||||
<span class="btn-icon">⚡</span>
|
||||
<span class="btn-text">din</span>
|
||||
</button>
|
||||
<p class="hint">点击任意处记录 · 长按快捷输入</p>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<header>
|
||||
<h1>🦐 din</h1>
|
||||
<p class="subtitle">Do It Now - 想到就做</p>
|
||||
</header>
|
||||
|
||||
<!-- 统计面板 -->
|
||||
<section class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="stat-today">0</div>
|
||||
<div class="stat-label">今日</div>
|
||||
<div class="stat-growth" id="growth-today">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="stat-week">0</div>
|
||||
<div class="stat-label">本周</div>
|
||||
<div class="stat-growth" id="growth-week">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="stat-month">0</div>
|
||||
<div class="stat-label">本月</div>
|
||||
<div class="stat-growth" id="growth-month">-</div>
|
||||
</div>
|
||||
<div class="stat-card total">
|
||||
<div class="stat-value" id="stat-total">0</div>
|
||||
<div class="stat-label">总计</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 大按钮 -->
|
||||
<section class="main-action">
|
||||
<button id="din-btn" class="din-button" aria-label="记录">
|
||||
<svg class="btn-icon" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="btnGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#ee5a5a;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 外发光圆环 -->
|
||||
<circle cx="60" cy="60" r="56" fill="none" stroke="rgba(255,107,107,0.3)" stroke-width="1"/>
|
||||
<!-- 主圆形背景 -->
|
||||
<circle cx="60" cy="60" r="50" fill="url(#btnGrad)"/>
|
||||
|
||||
<!-- 闪电图标 -->
|
||||
<path d="M68 30 L52 54 L62 54 L54 78 L72 50 L60 50 Z"
|
||||
fill="white"
|
||||
opacity="0.95"
|
||||
filter="drop-shadow(0 2px 4px rgba(0,0,0,0.2))"/>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="hint">点击记录 · 长按添加备注</p>
|
||||
</section>
|
||||
|
||||
<!-- 最近记录条 -->
|
||||
<section class="recent-strip">
|
||||
<div class="recent-header">
|
||||
<span class="recent-title">最近记录</span>
|
||||
<span class="view-all" id="btn-view-all">查看全部 →</span>
|
||||
</div>
|
||||
<div class="recent-chips" id="recent-chips">
|
||||
<div class="recent-chip empty">点击上方按钮开始</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 成就展示 -->
|
||||
<section class="achievements">
|
||||
<div class="achievements-header">
|
||||
<span class="achievements-title">成就</span>
|
||||
<span class="achievements-progress" id="ach-progress">0/24</span>
|
||||
</div>
|
||||
<div class="achievements-scroll" id="achievements-list">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 底部统计 -->
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item">
|
||||
<div class="stat-num" id="stat-today">0</div>
|
||||
<div class="stat-label">今日</div>
|
||||
<!-- 遮罩 -->
|
||||
<div class="overlay" id="overlay"></div>
|
||||
|
||||
<!-- 快速输入浮层 -->
|
||||
<div class="float-input" id="float-input">
|
||||
<div class="float-header">
|
||||
<span class="float-title">添加备注</span>
|
||||
<span class="float-time" id="float-time">--:--</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-num" id="stat-week">0</div>
|
||||
<div class="stat-label">本周</div>
|
||||
<input type="text" class="float-field" id="float-field" placeholder="做了什么?(可选)" autocomplete="off">
|
||||
|
||||
<div class="tags">
|
||||
<span class="tag" data-text="工作">💼 工作</span>
|
||||
<span class="tag" data-text="学习">📚 学习</span>
|
||||
<span class="tag" data-text="运动">💪 运动</span>
|
||||
<span class="tag" data-text="休息">😴 休息</span>
|
||||
<span class="tag" data-text="创意">💡 创意</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-num total" id="stat-total">0</div>
|
||||
<div class="stat-label">总计</div>
|
||||
|
||||
<div class="float-actions">
|
||||
<button class="btn btn-danger" id="btn-delete">🗑️ 删除</button>
|
||||
<button class="btn btn-secondary" id="btn-skip">跳过</button>
|
||||
<button class="btn btn-primary" id="btn-save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成就按钮 -->
|
||||
<button class="achievements-btn" id="achievements-btn" title="成就">🏆</button>
|
||||
|
||||
<!-- 最近记录面板 -->
|
||||
<div class="recent-panel" id="recent-panel">
|
||||
<div class="recent-title">
|
||||
<span>最近记录</span>
|
||||
<span class="view-all" id="view-all">查看全部 →</span>
|
||||
<!-- 历史记录面板(侧滑) -->
|
||||
<div class="history-panel" id="history-panel">
|
||||
<div class="history-header">
|
||||
<span class="history-title">历史记录</span>
|
||||
<button class="history-close" id="history-close">✕</button>
|
||||
</div>
|
||||
<div class="recent-list" id="recent-list">
|
||||
<div class="recent-item">
|
||||
<span class="recent-content empty">点击大按钮开始记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录面板(全屏) -->
|
||||
<div class="history-overlay" id="history-overlay">
|
||||
<div class="history-panel">
|
||||
<div class="history-header">
|
||||
<h2>历史记录</h2>
|
||||
<button class="close-btn" id="close-history">✕</button>
|
||||
</div>
|
||||
<div class="history-list" id="history-list">
|
||||
<!-- 动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速输入面板 -->
|
||||
<div class="input-overlay" id="input-overlay">
|
||||
<div class="input-panel">
|
||||
<div class="input-header">
|
||||
<span class="input-title">添加备注</span>
|
||||
<span class="timestamp" id="input-timestamp">--:--</span>
|
||||
</div>
|
||||
<input type="text" class="quick-input" id="quick-input" placeholder="做了什么?(可选)" autocomplete="off">
|
||||
<div class="quick-tags" id="quick-tags">
|
||||
<span class="tag" data-text="工作">💼 工作</span>
|
||||
<span class="tag" data-text="学习">📚 学习</span>
|
||||
<span class="tag" data-text="运动">💪 运动</span>
|
||||
<span class="tag" data-text="休息">😴 休息</span>
|
||||
<span class="tag" data-text="创意">💡 创意</span>
|
||||
<span class="tag" data-text="其他">📝 其他</span>
|
||||
</div>
|
||||
<div class="input-actions">
|
||||
<button class="btn btn-skip" id="btn-skip">跳过</button>
|
||||
<button class="btn btn-save" id="btn-save">保存</button>
|
||||
</div>
|
||||
<div class="history-list" id="history-list">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast">已记录!</div>
|
||||
|
||||
<!-- 成就解锁提示 -->
|
||||
<div class="achievement-toast" id="achievement-toast">
|
||||
<span class="achievement-icon">🏆</span>
|
||||
<span id="achievement-text">解锁成就</span>
|
||||
|
||||
<!-- 自定义确认弹窗 -->
|
||||
<div class="confirm-overlay" id="confirm-overlay">
|
||||
<div class="confirm-box">
|
||||
<div class="confirm-icon">🗑️</div>
|
||||
<div class="confirm-title">确认删除</div>
|
||||
<div class="confirm-message">确定要删除这条记录吗?</div>
|
||||
<div class="confirm-actions">
|
||||
<button class="confirm-btn cancel" id="confirm-cancel">取消</button>
|
||||
<button class="confirm-btn delete" id="confirm-ok">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
|
||||
855
static/style.css
855
static/style.css
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user