重构前端:极简极速交互设计
- 全屏点击即可记录,响应更快 - 长按直接进入输入模式 - 快速标签:工作/学习/运动/休息/创意/其他 - 底部常驻统计,无需切换页面 - 右侧面板显示最近记录 - 成就解锁动画提示 - 优化移动端触摸体验
This commit is contained in:
parent
b2a17d2aa0
commit
3eaecd18da
551
static/app.js
551
static/app.js
@ -1,256 +1,129 @@
|
|||||||
// API 基础地址
|
|
||||||
const API_BASE = '';
|
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 currentRecordId = null;
|
||||||
|
let lastRecordTime = 0;
|
||||||
|
let longPressTimer = null;
|
||||||
|
let isLongPress = false;
|
||||||
|
const LONG_PRESS_DURATION = 500; // 长按触发时间
|
||||||
|
|
||||||
// 初始化
|
// DOM 元素
|
||||||
async function init() {
|
const triggerArea = document.getElementById('trigger-area');
|
||||||
await initDB();
|
const dinBtn = document.getElementById('din-btn');
|
||||||
await loadStats();
|
const inputOverlay = document.getElementById('input-overlay');
|
||||||
await loadRecent();
|
const quickInput = document.getElementById('quick-input');
|
||||||
await loadAchievements();
|
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 = '<div class="empty">还没有记录,点击上方按钮开始!</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.innerHTML = records.map(r => `
|
|
||||||
<div class="recent-item" data-id="${r.id}">
|
|
||||||
<div class="recent-time">${formatTime(r.created_at)}</div>
|
|
||||||
<div class="recent-content ${!r.content ? 'empty' : ''}">${r.content || '(无描述)'}</div>
|
|
||||||
<div class="recent-actions">
|
|
||||||
<button onclick="editRecord(${r.id}, '${escapeHtml(r.content || '')}')" title="编辑">✏️</button>
|
|
||||||
<button onclick="deleteRecord(${r.id})" title="删除">🗑️</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).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 => `
|
|
||||||
<div class="achievement-item ${a.unlocked ? 'unlocked' : ''}" title="${a.desc}">
|
|
||||||
<div class="achievement-icon">${a.icon}</div>
|
|
||||||
<div class="achievement-name">${a.name}</div>
|
|
||||||
<div class="achievement-desc">${a.desc}</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载成就失败:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击大按钮
|
|
||||||
dinBtn.addEventListener('click', async () => {
|
|
||||||
dinBtn.classList.add('recording');
|
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 {
|
try {
|
||||||
// 检查网络状态
|
// 立即显示反馈(不等待网络)
|
||||||
if (!navigator.onLine) {
|
showToast('已记录!');
|
||||||
// 离线模式:存入队列
|
|
||||||
await addToQueue({ content });
|
|
||||||
await registerSync();
|
|
||||||
showToast('已保存,将在联网时同步');
|
|
||||||
|
|
||||||
// 显示输入区域(离线编辑)
|
|
||||||
currentRecordId = 'offline_' + Date.now();
|
|
||||||
inputSection.classList.remove('hidden');
|
|
||||||
dinContent.value = '';
|
|
||||||
dinContent.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 后台发送请求
|
||||||
const res = await fetch(`${API_BASE}/api/din`, {
|
const res = await fetch(`${API_BASE}/api/din`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content })
|
body: JSON.stringify({ content: '' })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
currentRecordId = data.id;
|
currentRecordId = data.id;
|
||||||
|
|
||||||
// 显示输入区域
|
// 更新统计和列表
|
||||||
inputSection.classList.remove('hidden');
|
updateStats();
|
||||||
dinContent.value = '';
|
prependToList(data);
|
||||||
dinContent.focus();
|
|
||||||
|
|
||||||
// 更新统计
|
// 检查成就
|
||||||
await loadStats();
|
checkNewAchievements();
|
||||||
|
|
||||||
|
// 如果需要输入,显示输入面板
|
||||||
|
if (showInput) {
|
||||||
|
showInputPanel(data.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('创建记录失败:', err);
|
console.error('记录失败:', err);
|
||||||
showToast('记录失败,请重试', false);
|
showToast('记录失败', false);
|
||||||
} finally {
|
|
||||||
dinBtn.classList.remove('recording');
|
|
||||||
dinBtn.disabled = 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;
|
if (!currentRecordId) return;
|
||||||
|
|
||||||
const content = dinContent.value.trim();
|
const content = quickInput.value.trim();
|
||||||
|
if (!content) {
|
||||||
|
hideInputPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_BASE}/api/din/${currentRecordId}`, {
|
await fetch(`${API_BASE}/api/din/${currentRecordId}`, {
|
||||||
@ -259,129 +132,177 @@ saveBtn.addEventListener('click', async () => {
|
|||||||
body: JSON.stringify({ content })
|
body: JSON.stringify({ content })
|
||||||
});
|
});
|
||||||
|
|
||||||
inputSection.classList.add('hidden');
|
// 更新列表中的内容
|
||||||
currentRecordId = null;
|
updateListItem(currentRecordId, content);
|
||||||
|
hideInputPanel();
|
||||||
await loadRecent();
|
showToast('已保存');
|
||||||
await loadAchievements();
|
|
||||||
showToast('记录成功!');
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('保存失败:', err);
|
|
||||||
showToast('保存失败', false);
|
showToast('保存失败', false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 跳过
|
// 跳过按钮
|
||||||
skipBtn.addEventListener('click', async () => {
|
document.getElementById('btn-skip').addEventListener('click', () => {
|
||||||
inputSection.classList.add('hidden');
|
hideInputPanel();
|
||||||
currentRecordId = null;
|
});
|
||||||
await loadRecent();
|
|
||||||
await loadAchievements();
|
// 点击遮罩关闭
|
||||||
showToast('记录成功!');
|
inputOverlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === inputOverlay) {
|
||||||
|
hideInputPanel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 回车保存
|
// 回车保存
|
||||||
dinContent.addEventListener('keypress', (e) => {
|
quickInput.addEventListener('keypress', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
saveBtn.click();
|
document.getElementById('btn-save').click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击外部自动跳过(不丢失记录)
|
// 快速标签
|
||||||
document.addEventListener('click', (e) => {
|
document.querySelectorAll('.tag').forEach(tag => {
|
||||||
// 输入框隐藏时不处理
|
tag.addEventListener('click', () => {
|
||||||
if (inputSection.classList.contains('hidden')) return;
|
const text = tag.dataset.text;
|
||||||
|
quickInput.value = text;
|
||||||
// 点击输入框内部不处理
|
document.getElementById('btn-save').click();
|
||||||
if (inputSection.contains(e.target)) return;
|
});
|
||||||
|
|
||||||
// 点击大按钮时不处理(刚点击过)
|
|
||||||
if (dinBtn.contains(e.target)) return;
|
|
||||||
|
|
||||||
// 点击其他地方:自动跳过
|
|
||||||
skipBtn.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 编辑记录
|
// ========== 数据更新 ==========
|
||||||
async function editRecord(id, currentContent) {
|
|
||||||
const newContent = prompt('修改内容:', currentContent);
|
async function updateStats() {
|
||||||
if (newContent === null) return; // 取消
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_BASE}/api/din/${id}`, {
|
const res = await fetch(`${API_BASE}/api/stats`);
|
||||||
method: 'PUT',
|
const stats = await res.json();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ content: newContent.trim() })
|
|
||||||
});
|
|
||||||
|
|
||||||
await loadRecent();
|
document.getElementById('stat-today').textContent = stats.today;
|
||||||
showToast('修改成功!');
|
document.getElementById('stat-week').textContent = stats.week;
|
||||||
|
document.getElementById('stat-total').textContent = stats.total;
|
||||||
|
document.getElementById('recent-count').textContent = stats.total;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('修改失败:', err);
|
console.error('更新统计失败:', err);
|
||||||
showToast('修改失败', false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除记录
|
function prependToList(record) {
|
||||||
async function deleteRecord(id) {
|
const list = document.getElementById('recent-list');
|
||||||
if (!confirm('确定删除这条记录?')) return;
|
const emptyMsg = list.querySelector('.recent-item .empty');
|
||||||
|
if (emptyMsg) {
|
||||||
|
list.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const item = document.createElement('div');
|
||||||
await fetch(`${API_BASE}/api/din/${id}`, { method: 'DELETE' });
|
item.className = 'recent-item';
|
||||||
|
item.dataset.id = record.id;
|
||||||
await loadRecent();
|
item.innerHTML = `
|
||||||
await loadStats();
|
<span class="recent-time">${formatTimeOnly(record.created_at)}</span>
|
||||||
await loadAchievements();
|
<span class="recent-content empty">(未备注)</span>
|
||||||
showToast('删除成功');
|
`;
|
||||||
} catch (err) {
|
|
||||||
console.error('删除失败:', err);
|
list.insertBefore(item, list.firstChild);
|
||||||
showToast('删除失败', false);
|
|
||||||
|
// 保持最多10条
|
||||||
|
while (list.children.length > 10) {
|
||||||
|
list.removeChild(list.lastChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示提示
|
function updateListItem(id, content) {
|
||||||
function showToast(text, success = true) {
|
const item = document.querySelector(`.recent-item[data-id="${id}"]`);
|
||||||
toast.querySelector('.toast-text').textContent = text;
|
if (item) {
|
||||||
toast.querySelector('.toast-icon').textContent = success ? '✓' : '✗';
|
const contentEl = item.querySelector('.recent-content');
|
||||||
toast.style.background = success ? 'rgba(74,222,128,0.9)' : 'rgba(248,113,113,0.9)';
|
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(() => {
|
setTimeout(() => {
|
||||||
toast.classList.add('hidden');
|
achievementToast.classList.remove('show');
|
||||||
}, 2000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化时间
|
// ========== 辅助函数 ==========
|
||||||
function formatTime(isoString) {
|
|
||||||
const date = new Date(isoString);
|
function showToast(message, success = true) {
|
||||||
const now = new Date();
|
toast.textContent = message;
|
||||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
toast.style.background = success ? '#ff4757' : '#ff6b6b';
|
||||||
const recordDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
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 hours = date.getHours().toString().padStart(2, '0');
|
||||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
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()}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML 转义
|
// ========== 初始化 ==========
|
||||||
function escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
async function init() {
|
||||||
div.textContent = text;
|
await updateStats();
|
||||||
return div.innerHTML;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动
|
// 启动
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<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="#1a1a2e">
|
<meta name="theme-color" content="#0d0d0f">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<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-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="din">
|
<meta name="apple-mobile-web-app-title" content="din">
|
||||||
@ -12,83 +12,83 @@
|
|||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="apple-touch-icon" href="icon-192.png">
|
<link rel="apple-touch-icon" href="icon-192.png">
|
||||||
<script>
|
<script>
|
||||||
// 注册 Service Worker
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/sw.js')
|
navigator.serviceWorker.register('/sw.js');
|
||||||
.then(reg => console.log('SW registered'))
|
|
||||||
.catch(err => console.log('SW error:', err));
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<!-- 主触发区 - 全屏点击即可记录 -->
|
||||||
<!-- 头部 -->
|
<div class="din-trigger" id="trigger-area">
|
||||||
<header>
|
<button class="big-btn" id="din-btn" aria-label="记录 din">
|
||||||
<h1>🦐 din</h1>
|
<span class="btn-icon">⚡</span>
|
||||||
<p class="subtitle">Do It Now - 想到就做</p>
|
<span class="btn-text">din</span>
|
||||||
</header>
|
</button>
|
||||||
|
<p class="hint">点击任意处记录 · 长按快捷输入</p>
|
||||||
<!-- 统计面板 -->
|
|
||||||
<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">
|
|
||||||
<span class="btn-text">🔴</span>
|
|
||||||
<span class="btn-label">Do It Now</span>
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 快速输入(点击后显示) -->
|
|
||||||
<section id="input-section" class="input-section hidden">
|
|
||||||
<input type="text" id="din-content" placeholder="做了什么?(可选,直接回车跳过)" maxlength="100">
|
|
||||||
<div class="input-actions">
|
|
||||||
<button id="save-btn" class="btn-primary">保存</button>
|
|
||||||
<button id="skip-btn" class="btn-secondary">跳过</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 最近记录 -->
|
|
||||||
<section class="recent">
|
|
||||||
<h2>📝 最近记录</h2>
|
|
||||||
<div id="recent-list" class="recent-list">
|
|
||||||
<div class="empty">还没有记录,点击上方按钮开始!</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 成就 -->
|
|
||||||
<section class="achievements">
|
|
||||||
<h2>🏆 成就 <span id="achievement-progress">(0/8)</span></h2>
|
|
||||||
<div id="achievement-list" class="achievement-list">
|
|
||||||
<!-- 动态生成 -->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 成功提示 -->
|
<!-- 底部统计 -->
|
||||||
<div id="toast" class="toast hidden">
|
<div class="stats-bar">
|
||||||
<span class="toast-icon">✓</span>
|
<div class="stat-item">
|
||||||
<span class="toast-text">记录成功!</span>
|
<div class="stat-num" id="stat-today">0</div>
|
||||||
|
<div class="stat-label">今日</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-num" id="stat-week">0</div>
|
||||||
|
<div class="stat-label">本周</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-num total" id="stat-total">0</div>
|
||||||
|
<div class="stat-label">总计</div>
|
||||||
|
</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="recent-count" id="recent-count">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="recent-list" id="recent-list">
|
||||||
|
<div class="recent-item">
|
||||||
|
<span class="recent-content empty">点击大按钮开始记录</span>
|
||||||
|
</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>
|
||||||
|
</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>
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|||||||
578
static/style.css
578
static/style.css
@ -2,370 +2,446 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
background: #0d0d0f;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
/* 主按钮 - 占据屏幕中心 */
|
||||||
max-width: 600px;
|
.din-trigger {
|
||||||
margin: 0 auto;
|
position: fixed;
|
||||||
}
|
top: 0;
|
||||||
|
left: 0;
|
||||||
/* 头部 */
|
width: 100%;
|
||||||
header {
|
height: 100%;
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 统计面板 */
|
|
||||||
.stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 15px 10px;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card.total {
|
|
||||||
background: rgba(255,107,107,0.15);
|
|
||||||
border-color: rgba(255,107,107,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card.total .stat-value {
|
|
||||||
color: #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #888;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-growth {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-growth.negative {
|
|
||||||
color: #f87171;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 大按钮 */
|
|
||||||
.main-action {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 20px;
|
z-index: 10;
|
||||||
|
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0d0d0f 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.din-button {
|
.big-btn {
|
||||||
width: 180px;
|
width: 200px;
|
||||||
height: 180px;
|
height: 200px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: none;
|
border: none;
|
||||||
background: linear-gradient(145deg, #ff6b6b, #ee5a5a);
|
background: linear-gradient(145deg, #ff4757, #ff3838);
|
||||||
box-shadow: 0 10px 40px rgba(255,107,107,0.4),
|
box-shadow:
|
||||||
inset 0 -5px 20px rgba(0,0,0,0.2),
|
0 20px 60px rgba(255, 71, 87, 0.4),
|
||||||
inset 0 5px 20px rgba(255,255,255,0.2);
|
0 0 0 20px rgba(255, 71, 87, 0.1),
|
||||||
|
inset 0 -4px 20px rgba(0,0,0,0.2),
|
||||||
|
inset 0 4px 20px rgba(255,255,255,0.2);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transition: all 0.2s ease;
|
user-select: none;
|
||||||
position: relative;
|
-webkit-user-select: none;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.din-button:hover {
|
.big-btn:active {
|
||||||
transform: scale(1.05);
|
transform: scale(0.92);
|
||||||
box-shadow: 0 15px 50px rgba(255,107,107,0.5),
|
box-shadow:
|
||||||
inset 0 -5px 20px rgba(0,0,0,0.2),
|
0 10px 30px rgba(255, 71, 87, 0.3),
|
||||||
inset 0 5px 20px rgba(255,255,255,0.2);
|
0 0 0 10px rgba(255, 71, 87, 0.05),
|
||||||
|
inset 0 -2px 10px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.din-button:active {
|
.big-btn.recording {
|
||||||
transform: scale(0.95);
|
animation: pulse-record 0.6s ease-out;
|
||||||
box-shadow: 0 5px 20px rgba(255,107,107,0.3),
|
|
||||||
inset 0 -3px 10px rgba(0,0,0,0.2),
|
|
||||||
inset 0 3px 10px rgba(255,255,255,0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.din-button.recording {
|
@keyframes pulse-record {
|
||||||
animation: pulse 1s infinite;
|
0% { transform: scale(1); box-shadow: 0 20px 60px rgba(255, 71, 87, 0.4), 0 0 0 20px rgba(255, 71, 87, 0.1); }
|
||||||
|
50% { transform: scale(1.05); box-shadow: 0 30px 80px rgba(255, 71, 87, 0.6), 0 0 0 40px rgba(255, 71, 87, 0); }
|
||||||
|
100% { transform: scale(1); box-shadow: 0 20px 60px rgba(255, 71, 87, 0.4), 0 0 0 20px rgba(255, 71, 87, 0.1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
.btn-icon {
|
||||||
0%, 100% { box-shadow: 0 10px 40px rgba(255,107,107,0.4); }
|
font-size: 4rem;
|
||||||
50% { box-shadow: 0 10px 60px rgba(255,107,107,0.7); }
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-text {
|
.btn-text {
|
||||||
font-size: 3rem;
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 30px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部统计 - 常驻显示 */
|
||||||
|
.stats-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(transparent, rgba(13,13,15,0.95) 40%);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-label {
|
.stat-num.total {
|
||||||
font-size: 1rem;
|
color: #ff4757;
|
||||||
font-weight: 600;
|
}
|
||||||
color: #fff;
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入区域 */
|
/* 快速输入层 - 从底部滑出 */
|
||||||
.input-section {
|
.input-overlay {
|
||||||
background: rgba(255,255,255,0.05);
|
position: fixed;
|
||||||
border-radius: 12px;
|
top: 0;
|
||||||
padding: 20px;
|
left: 0;
|
||||||
margin-bottom: 30px;
|
right: 0;
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-section.hidden {
|
.input-overlay.active {
|
||||||
display: none;
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
#din-content {
|
.input-panel {
|
||||||
|
background: #1a1a1f;
|
||||||
|
border-radius: 24px 24px 0 0;
|
||||||
|
padding: 24px;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-overlay.active .input-panel {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
font-family: 'SF Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 15px;
|
background: #0d0d0f;
|
||||||
font-size: 1rem;
|
border: 2px solid #333;
|
||||||
border: 2px solid rgba(255,255,255,0.1);
|
border-radius: 16px;
|
||||||
border-radius: 8px;
|
padding: 18px 20px;
|
||||||
background: rgba(0,0,0,0.2);
|
font-size: 1.1rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-bottom: 15px;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#din-content:focus {
|
.quick-input:focus {
|
||||||
border-color: #ff6b6b;
|
border-color: #ff4757;
|
||||||
}
|
}
|
||||||
|
|
||||||
#din-content::placeholder {
|
.quick-input::placeholder {
|
||||||
color: #666;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-actions {
|
/* 快速标签 */
|
||||||
|
.quick-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary, .btn-secondary {
|
.tag {
|
||||||
flex: 1;
|
padding: 8px 16px;
|
||||||
padding: 12px 20px;
|
background: #25252a;
|
||||||
border-radius: 8px;
|
border: 1px solid #333;
|
||||||
border: none;
|
border-radius: 20px;
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
|
color: #aaa;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.tag:hover, .tag:active {
|
||||||
background: #ff6b6b;
|
background: #ff4757;
|
||||||
|
border-color: #ff4757;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
/* 操作按钮 */
|
||||||
background: #ff5252;
|
.input-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn {
|
||||||
background: rgba(255,255,255,0.1);
|
flex: 1;
|
||||||
color: #aaa;
|
padding: 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-save {
|
||||||
background: rgba(255,255,255,0.15);
|
background: #ff4757;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 最近记录 */
|
.btn-save:active {
|
||||||
.recent {
|
background: #ff3838;
|
||||||
margin-bottom: 30px;
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent h2, .achievements h2 {
|
.btn-skip {
|
||||||
font-size: 1.1rem;
|
background: #25252a;
|
||||||
margin-bottom: 15px;
|
color: #888;
|
||||||
color: #aaa;
|
}
|
||||||
|
|
||||||
|
.btn-skip:active {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最近记录 - 右侧面板 (桌面) / 底部 (手机) */
|
||||||
|
.recent-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 280px;
|
||||||
|
max-height: calc(100vh - 140px);
|
||||||
|
background: rgba(26,26,31,0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-count {
|
||||||
|
color: #ff4757;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-list {
|
.recent-list {
|
||||||
background: rgba(255,255,255,0.03);
|
display: flex;
|
||||||
border-radius: 12px;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-item {
|
.recent-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
padding: 15px;
|
gap: 12px;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
padding: 12px;
|
||||||
gap: 15px;
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-item:last-child {
|
.recent-item:hover {
|
||||||
border-bottom: none;
|
background: rgba(255,255,255,0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-time {
|
.recent-time {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #666;
|
color: #555;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
min-width: 60px;
|
font-family: 'SF Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-content {
|
.recent-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
font-size: 0.9rem;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-content.empty {
|
.recent-content.empty {
|
||||||
color: #666;
|
color: #555;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-actions button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #666;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 5px;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-actions button:hover {
|
|
||||||
color: #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 成就 */
|
|
||||||
.achievement-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievement-item {
|
|
||||||
background: rgba(255,255,255,0.03);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 15px 10px;
|
|
||||||
text-align: center;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: all 0.2s;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievement-item.unlocked {
|
|
||||||
background: rgba(255,215,0,0.1);
|
|
||||||
border-color: rgba(255,215,0,0.3);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievement-icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievement-name {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #aaa;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.achievement-desc {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast */
|
/* Toast */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 30px;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translate(-50%, -50%) scale(0.8);
|
||||||
background: rgba(74,222,128,0.9);
|
background: #ff4757;
|
||||||
color: #000;
|
color: #fff;
|
||||||
padding: 15px 30px;
|
padding: 16px 32px;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 200;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成就徽章 */
|
||||||
|
.achievement-toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-100px);
|
||||||
|
background: linear-gradient(135deg, #ffd700, #ffaa00);
|
||||||
|
color: #000;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
z-index: 200;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.hidden {
|
.achievement-toast.show {
|
||||||
opacity: 0;
|
transform: translateX(-50%) translateY(0);
|
||||||
transform: translateX(-50%) translateY(20px);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式 */
|
.achievement-icon {
|
||||||
@media (max-width: 500px) {
|
font-size: 1.8rem;
|
||||||
.stats {
|
}
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.big-btn {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievement-list {
|
.btn-icon {
|
||||||
grid-template-columns: repeat(4, 1fr);
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.achievement-name {
|
.recent-panel {
|
||||||
font-size: 0.65rem;
|
position: fixed;
|
||||||
|
top: auto;
|
||||||
|
bottom: 100px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
background: rgba(13,13,15,0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.din-button {
|
.stats-bar {
|
||||||
width: 150px;
|
padding-bottom: 30px;
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-text {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏的成就面板 - 可展开 */
|
||||||
|
.achievements-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 30;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑动指示器 */
|
||||||
|
.swipe-indicator {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 100px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-dot.active {
|
||||||
|
background: #ff4757;
|
||||||
|
width: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user