优化倒计时排版布局
Vercel Deploy / deploy (push) Successful in 1m2s

This commit is contained in:
2026-04-26 19:30:14 +08:00
Unverified
parent a838ed0564
commit 923525b6d1
2 changed files with 195 additions and 32 deletions
+28
View File
@@ -429,6 +429,30 @@ label {
background: rgba(248, 251, 255, 0.86); background: rgba(248, 251, 255, 0.86);
} }
.template-item.dragging {
opacity: 0.55;
}
.template-item.drag-over {
border-color: rgba(15, 98, 254, 0.5);
box-shadow: inset 0 0 0 1px rgba(15, 98, 254, 0.18);
}
.countdown-grid-drag-handle {
width: 34px;
height: 34px;
border: 1px dashed rgba(121, 140, 176, 0.4);
border-radius: 6px;
background: rgba(255, 255, 255, 0.92);
color: var(--muted-text);
cursor: grab;
flex: 0 0 auto;
}
.countdown-grid-drag-handle:active {
cursor: grabbing;
}
.template-item input[type="text"] { .template-item input[type="text"] {
min-width: 120px; min-width: 120px;
flex: 1 1 160px; flex: 1 1 160px;
@@ -438,6 +462,10 @@ label {
min-width: 150px; min-width: 150px;
} }
.template-item input[type="number"] {
width: 72px;
}
.template-item label { .template-item label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+167 -32
View File
@@ -4,6 +4,7 @@ let startTime, msgIndex, appVersion;
let canvas, ctx, textDecoder; let canvas, ctx, textDecoder;
let paintManager, cropManager; let paintManager, cropManager;
let activeCanvasPreset = null; let activeCanvasPreset = null;
let draggedCountdownIndex = null;
const EpdCmd = { const EpdCmd = {
SET_PINS: 0x00, SET_PINS: 0x00,
@@ -55,6 +56,7 @@ const canvasSizes = [
const countdownStorageKey = 'epdCountdownTemplates'; const countdownStorageKey = 'epdCountdownTemplates';
const countdownImportVersion = 1; const countdownImportVersion = 1;
const minimumCountdownWeight = 0;
const todoStorageKey = 'epdTodoTemplates'; const todoStorageKey = 'epdTodoTemplates';
const todoImportVersion = 1; const todoImportVersion = 1;
const defaultCountdownState = { const defaultCountdownState = {
@@ -67,8 +69,8 @@ const defaultCountdownState = {
grid: { grid: {
title: '倒计时看板', title: '倒计时看板',
items: [ items: [
{ name: '期末考试', date: '2026-06-20', important: true }, { name: '期末考试', date: '2026-06-20', important: true, weight: 2 },
{ name: '驾照考试', date: '2026-05-01', important: false } { name: '驾照考试', date: '2026-05-01', important: false, weight: 1 }
] ]
} }
}; };
@@ -96,6 +98,11 @@ function cloneDefaultTodoState() {
return JSON.parse(JSON.stringify(defaultTodoState)); return JSON.parse(JSON.stringify(defaultTodoState));
} }
function sanitizeCountdownWeight(value) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed >= minimumCountdownWeight ? parsed : minimumCountdownWeight;
}
function normalizeCountdownState(state) { function normalizeCountdownState(state) {
const normalized = cloneDefaultCountdownState(); const normalized = cloneDefaultCountdownState();
if (!state || typeof state !== 'object') return normalized; if (!state || typeof state !== 'object') return normalized;
@@ -112,7 +119,8 @@ function normalizeCountdownState(state) {
normalized.grid.items = state.grid.items.map((item) => ({ normalized.grid.items = state.grid.items.map((item) => ({
name: item.name || '未命名', name: item.name || '未命名',
date: item.date || getTodayISODate(), date: item.date || getTodayISODate(),
important: !!item.important important: !!item.important,
weight: sanitizeCountdownWeight(item.weight)
})); }));
} }
} }
@@ -641,10 +649,11 @@ function syncCountdownFormToState() {
countdownState.grid.items = Array.from(document.querySelectorAll('#countdown-grid-items .template-item')).map((item) => ({ countdownState.grid.items = Array.from(document.querySelectorAll('#countdown-grid-items .template-item')).map((item) => ({
name: item.querySelector('.countdown-grid-name').value.trim() || '未命名', name: item.querySelector('.countdown-grid-name').value.trim() || '未命名',
date: item.querySelector('.countdown-grid-date').value || getTodayISODate(), date: item.querySelector('.countdown-grid-date').value || getTodayISODate(),
important: item.querySelector('.countdown-grid-important').checked important: item.querySelector('.countdown-grid-important').checked,
weight: sanitizeCountdownWeight(item.querySelector('.countdown-grid-weight').value)
})); }));
if (countdownState.grid.items.length === 0) { if (countdownState.grid.items.length === 0) {
countdownState.grid.items.push({ name: '未命名', date: getTodayISODate(), important: false }); countdownState.grid.items.push({ name: '未命名', date: getTodayISODate(), important: false, weight: 1 });
} }
} }
@@ -672,16 +681,49 @@ function renderCountdownGridItems() {
countdownState.grid.items.forEach((item, index) => { countdownState.grid.items.forEach((item, index) => {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'template-item'; row.className = 'template-item';
row.draggable = true;
row.dataset.countdownIndex = String(index);
row.innerHTML = ` row.innerHTML = `
<button type="button" class="countdown-grid-drag-handle" title="拖动排序" aria-label="拖动排序">☰</button>
<input type="text" class="countdown-grid-name" value="${escapeHtml(item.name)}"> <input type="text" class="countdown-grid-name" value="${escapeHtml(item.name)}">
<input type="date" class="countdown-grid-date" value="${escapeHtml(item.date)}"> <input type="date" class="countdown-grid-date" value="${escapeHtml(item.date)}">
<label>权重 <input type="number" class="countdown-grid-weight" min="${minimumCountdownWeight}" step="1" value="${sanitizeCountdownWeight(item.weight)}"></label>
<label><input type="checkbox" class="countdown-grid-important" ${item.important ? 'checked' : ''}> 重要</label> <label><input type="checkbox" class="countdown-grid-important" ${item.important ? 'checked' : ''}> 重要</label>
<button type="button" class="secondary countdown-grid-remove">删除</button> <button type="button" class="secondary countdown-grid-remove">删除</button>
`; `;
row.addEventListener('dragstart', (event) => {
draggedCountdownIndex = index;
row.classList.add('dragging');
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', String(index));
}
});
row.addEventListener('dragend', () => {
draggedCountdownIndex = null;
row.classList.remove('dragging');
document.querySelectorAll('#countdown-grid-items .template-item').forEach((element) => {
element.classList.remove('drag-over');
});
});
row.addEventListener('dragover', (event) => {
event.preventDefault();
if (draggedCountdownIndex === null || draggedCountdownIndex === index) return;
row.classList.add('drag-over');
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
});
row.addEventListener('dragleave', () => {
row.classList.remove('drag-over');
});
row.addEventListener('drop', (event) => {
event.preventDefault();
row.classList.remove('drag-over');
moveCountdownGridItem(index);
});
row.querySelector('.countdown-grid-remove').addEventListener('click', () => { row.querySelector('.countdown-grid-remove').addEventListener('click', () => {
countdownState.grid.items.splice(index, 1); countdownState.grid.items.splice(index, 1);
if (countdownState.grid.items.length === 0) { if (countdownState.grid.items.length === 0) {
countdownState.grid.items.push({ name: '未命名', date: getTodayISODate(), important: false }); countdownState.grid.items.push({ name: '未命名', date: getTodayISODate(), important: false, weight: 1 });
} }
renderCountdownGridItems(); renderCountdownGridItems();
saveCountdownState(); saveCountdownState();
@@ -690,6 +732,17 @@ function renderCountdownGridItems() {
}); });
} }
function moveCountdownGridItem(targetIndex) {
if (draggedCountdownIndex === null || draggedCountdownIndex === targetIndex) return;
syncCountdownFormToState();
const [movedItem] = countdownState.grid.items.splice(draggedCountdownIndex, 1);
if (!movedItem) return;
countdownState.grid.items.splice(targetIndex, 0, movedItem);
renderCountdownGridItems();
saveCountdownState();
}
function updateCountdownModeUI() { function updateCountdownModeUI() {
const mode = document.getElementById('countdown-template-mode').value; const mode = document.getElementById('countdown-template-mode').value;
document.getElementById('countdown-single-panel').style.display = mode === 'single' ? 'block' : 'none'; document.getElementById('countdown-single-panel').style.display = mode === 'single' ? 'block' : 'none';
@@ -702,6 +755,82 @@ function refreshCountdownTemplateUI() {
updateCountdownModeUI(); updateCountdownModeUI();
} }
function getCountdownGridRowCount(items) {
if (items.length <= 2) return 1;
if (items.length <= 6) return 2;
return 3;
}
function buildSequentialCountdownLayout(items, x, y, width, height, gap) {
if (items.length === 0 || width <= 0 || height <= 0) return [];
if (items.length === 1) {
return [{ item: items[0], x, y, width, height }];
}
const normalizedItems = items.map((item) => ({
...item,
weight: Math.max(1, sanitizeCountdownWeight(item.weight))
}));
const rowCount = Math.min(getCountdownGridRowCount(normalizedItems), normalizedItems.length);
const totalWeight = normalizedItems.reduce((sum, item) => sum + item.weight, 0);
const rows = [];
let currentRow = [];
let currentRowWeight = 0;
let remainingWeight = totalWeight;
normalizedItems.forEach((item, index) => {
const rowsRemaining = rowCount - rows.length;
const itemsRemaining = normalizedItems.length - index;
const targetWeight = remainingWeight / Math.max(1, rowsRemaining);
const nextWeight = currentRowWeight + item.weight;
const canStartNextRow = rows.length < rowCount - 1 && currentRow.length > 0 && itemsRemaining >= rowsRemaining;
if (canStartNextRow && nextWeight > targetWeight) {
rows.push({ items: currentRow, weight: currentRowWeight });
remainingWeight -= currentRowWeight;
currentRow = [item];
currentRowWeight = item.weight;
return;
}
currentRow.push(item);
currentRowWeight = nextWeight;
});
if (currentRow.length > 0) {
rows.push({ items: currentRow, weight: currentRowWeight });
}
const usableHeight = height - gap * (rows.length - 1);
let yCursor = y;
return rows.flatMap((row, rowIndex) => {
const rowHeight = rowIndex === rows.length - 1
? y + height - yCursor
: usableHeight * (row.weight / totalWeight);
const usableWidth = width - gap * (row.items.length - 1);
let xCursor = x;
const layouts = row.items.map((item, itemIndex) => {
const itemWidth = itemIndex === row.items.length - 1
? x + width - xCursor
: usableWidth * (item.weight / row.weight);
const layout = {
item,
x: xCursor,
y: yCursor,
width: itemWidth,
height: rowHeight
};
xCursor += itemWidth + gap;
return layout;
});
yCursor += rowHeight + gap;
return layouts;
});
}
function syncTodoFormToState() { function syncTodoFormToState() {
todoState.title = document.getElementById('todo-template-title').value.trim() || '今日重点'; todoState.title = document.getElementById('todo-template-title').value.trim() || '今日重点';
todoState.note = document.getElementById('todo-template-note').value.trim() || '一次做好一件事'; todoState.note = document.getElementById('todo-template-note').value.trim() || '一次做好一件事';
@@ -954,7 +1083,12 @@ function drawGridCountdownTemplate() {
const scaleX = canvas.width / 400; const scaleX = canvas.width / 400;
const scaleY = canvas.height / 300; const scaleY = canvas.height / 300;
const scale = Math.min(scaleX, scaleY); const scale = Math.min(scaleX, scaleY);
const items = countdownState.grid.items.slice(0, 9); const items = countdownState.grid.items.slice(0, 9)
.map((item) => ({
...item,
weight: sanitizeCountdownWeight(item.weight)
}))
.filter((item) => item.weight > 0);
prepareTemplateCanvas(); prepareTemplateCanvas();
@@ -981,38 +1115,39 @@ function drawGridCountdownTemplate() {
const contentTop = 50 * scaleY; const contentTop = 50 * scaleY;
const contentHeight = canvas.height - contentTop - 12 * scaleY; const contentHeight = canvas.height - contentTop - 12 * scaleY;
const gap = 10 * scale; const gap = 10 * scale;
let cols = 2; const cardLayouts = buildSequentialCountdownLayout(items, 12 * scaleX, contentTop, canvas.width - 24 * scaleX, contentHeight, gap);
if (items.length <= 1) cols = 1;
else if (items.length > 4) cols = 3;
const rows = Math.max(1, Math.ceil(items.length / cols));
const cardWidth = (canvas.width - 24 * scaleX - gap * (cols - 1)) / cols;
const cardHeight = (contentHeight - gap * (rows - 1)) / rows;
const cardAreaRatio = Math.sqrt((cardWidth * cardHeight) / ((canvas.width * canvas.height) / 6));
const densityBoost = items.length <= 1 ? 1.65 : items.length === 2 ? 1.45 : items.length <= 4 ? 1.22 : 1;
items.forEach((item, index) => { if (cardLayouts.length === 0) {
const col = index % cols; ctx.fillStyle = '#666666';
const row = Math.floor(index / cols); ctx.textAlign = 'center';
const x = 12 * scaleX + col * (cardWidth + gap); ctx.textBaseline = 'middle';
const y = contentTop + row * (cardHeight + gap); ctx.font = `bold ${Math.max(14, 16 * scale)}px Microsoft YaHei`;
ctx.fillText('暂无显示项目', canvas.width / 2, contentTop + contentHeight / 2);
paintManager.saveToHistory();
return;
}
cardLayouts.forEach(({ item, x, y, width, height }) => {
const days = getCountdownDays(item.date); const days = getCountdownDays(item.date);
const cardAreaRatio = Math.sqrt((width * height) / ((canvas.width * canvas.height) / Math.max(1, items.length)));
const densityBoost = items.length <= 1 ? 1.65 : items.length === 2 ? 1.45 : items.length <= 4 ? 1.22 : 1;
drawRoundedRect(ctx, x, y, cardWidth, cardHeight, 6 * scale, item.important ? Math.max(2, 4 * scale) : Math.max(1, 2 * scale), item.important ? '#FF0000' : '#000000'); drawRoundedRect(ctx, x, y, width, height, 6 * scale, item.important ? Math.max(2, 4 * scale) : Math.max(1, 2 * scale), item.important ? '#FF0000' : '#000000');
if (item.important) { if (item.important) {
ctx.fillStyle = '#FF0000'; ctx.fillStyle = '#FF0000';
ctx.font = `bold ${Math.max(14, 18 * scale)}px Arial`; ctx.font = `bold ${Math.max(14, 18 * scale)}px Arial`;
ctx.textAlign = 'right'; ctx.textAlign = 'right';
ctx.fillText('*', x + cardWidth - 8 * scaleX, y + 14 * scaleY); ctx.fillText('*', x + width - 8 * scaleX, y + 14 * scaleY);
} }
const namePreferredSize = Math.min(cardHeight * 0.24, 24 * scale * cardAreaRatio * densityBoost); const namePreferredSize = Math.min(height * 0.24, 24 * scale * cardAreaRatio * densityBoost);
const nameMinSize = Math.max(11, 12 * scale); const nameMinSize = Math.max(11, 12 * scale);
const nameSize = fitText(ctx, item.name, cardWidth - 18 * scaleX, namePreferredSize, nameMinSize, 'Microsoft YaHei', 'bold'); const nameSize = fitText(ctx, item.name, width - 18 * scaleX, namePreferredSize, nameMinSize, 'Microsoft YaHei', 'bold');
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.font = `bold ${nameSize}px Microsoft YaHei`; ctx.font = `bold ${nameSize}px Microsoft YaHei`;
ctx.fillText(item.name, x + cardWidth / 2, y + cardHeight * 0.3); ctx.fillText(item.name, x + width / 2, y + height * 0.3);
let valueText = ''; let valueText = '';
let unitText = ''; let unitText = '';
@@ -1027,22 +1162,22 @@ function drawGridCountdownTemplate() {
unitText = '天'; unitText = '天';
} }
const numberPreferredSize = Math.min(cardHeight * 0.56, 56 * scale * cardAreaRatio * densityBoost); const numberPreferredSize = Math.min(height * 0.56, 56 * scale * cardAreaRatio * densityBoost);
const numberMinSize = Math.max(16, 18 * scale); const numberMinSize = Math.max(16, 18 * scale);
const numberSize = fitText(ctx, valueText, cardWidth * (unitText ? 0.5 : 0.72), numberPreferredSize, numberMinSize, 'Arial Black', '900'); const numberSize = fitText(ctx, valueText, width * (unitText ? 0.5 : 0.72), numberPreferredSize, numberMinSize, 'Arial Black', '900');
ctx.fillStyle = valueColor; ctx.fillStyle = valueColor;
ctx.font = `900 ${numberSize}px Arial Black`; ctx.font = `900 ${numberSize}px Arial Black`;
const centerY = y + cardHeight * 0.68; const centerY = y + height * 0.68;
if (unitText) { if (unitText) {
const numberWidth = ctx.measureText(valueText).width; const numberWidth = ctx.measureText(valueText).width;
ctx.fillText(valueText, x + cardWidth / 2 - 8 * scaleX, centerY); ctx.fillText(valueText, x + width / 2 - 8 * scaleX, centerY);
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.font = `bold ${Math.max(10, numberSize * 0.34)}px Microsoft YaHei`; ctx.font = `bold ${Math.max(10, numberSize * 0.34)}px Microsoft YaHei`;
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillText(unitText, x + cardWidth / 2 - 8 * scaleX + numberWidth / 2 + 4 * scaleX, centerY + 6 * scaleY); ctx.fillText(unitText, x + width / 2 - 8 * scaleX + numberWidth / 2 + 4 * scaleX, centerY + 6 * scaleY);
} else { } else {
ctx.fillText(valueText, x + cardWidth / 2, centerY); ctx.fillText(valueText, x + width / 2, centerY);
} }
}); });
@@ -1208,7 +1343,7 @@ function initCountdownTemplate() {
}); });
document.getElementById('countdown-grid-add').addEventListener('click', () => { document.getElementById('countdown-grid-add').addEventListener('click', () => {
syncCountdownFormToState(); syncCountdownFormToState();
countdownState.grid.items.push({ name: `项目 ${countdownState.grid.items.length + 1}`, date: getTodayISODate(), important: false }); countdownState.grid.items.push({ name: `项目 ${countdownState.grid.items.length + 1}`, date: getTodayISODate(), important: false, weight: 1 });
renderCountdownGridItems(); renderCountdownGridItems();
saveCountdownState(); saveCountdownState();
}); });