优化倒计时排版布局
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);
}
.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"] {
min-width: 120px;
flex: 1 1 160px;
@@ -438,6 +462,10 @@ label {
min-width: 150px;
}
.template-item input[type="number"] {
width: 72px;
}
.template-item label {
display: inline-flex;
align-items: center;
+167 -32
View File
@@ -4,6 +4,7 @@ let startTime, msgIndex, appVersion;
let canvas, ctx, textDecoder;
let paintManager, cropManager;
let activeCanvasPreset = null;
let draggedCountdownIndex = null;
const EpdCmd = {
SET_PINS: 0x00,
@@ -55,6 +56,7 @@ const canvasSizes = [
const countdownStorageKey = 'epdCountdownTemplates';
const countdownImportVersion = 1;
const minimumCountdownWeight = 0;
const todoStorageKey = 'epdTodoTemplates';
const todoImportVersion = 1;
const defaultCountdownState = {
@@ -67,8 +69,8 @@ const defaultCountdownState = {
grid: {
title: '倒计时看板',
items: [
{ name: '期末考试', date: '2026-06-20', important: true },
{ name: '驾照考试', date: '2026-05-01', important: false }
{ name: '期末考试', date: '2026-06-20', important: true, weight: 2 },
{ name: '驾照考试', date: '2026-05-01', important: false, weight: 1 }
]
}
};
@@ -96,6 +98,11 @@ function cloneDefaultTodoState() {
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) {
const normalized = cloneDefaultCountdownState();
if (!state || typeof state !== 'object') return normalized;
@@ -112,7 +119,8 @@ function normalizeCountdownState(state) {
normalized.grid.items = state.grid.items.map((item) => ({
name: item.name || '未命名',
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) => ({
name: item.querySelector('.countdown-grid-name').value.trim() || '未命名',
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) {
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) => {
const row = document.createElement('div');
row.className = 'template-item';
row.draggable = true;
row.dataset.countdownIndex = String(index);
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="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>
<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', () => {
countdownState.grid.items.splice(index, 1);
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();
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() {
const mode = document.getElementById('countdown-template-mode').value;
document.getElementById('countdown-single-panel').style.display = mode === 'single' ? 'block' : 'none';
@@ -702,6 +755,82 @@ function refreshCountdownTemplateUI() {
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() {
todoState.title = document.getElementById('todo-template-title').value.trim() || '今日重点';
todoState.note = document.getElementById('todo-template-note').value.trim() || '一次做好一件事';
@@ -954,7 +1083,12 @@ function drawGridCountdownTemplate() {
const scaleX = canvas.width / 400;
const scaleY = canvas.height / 300;
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();
@@ -981,38 +1115,39 @@ function drawGridCountdownTemplate() {
const contentTop = 50 * scaleY;
const contentHeight = canvas.height - contentTop - 12 * scaleY;
const gap = 10 * scale;
let cols = 2;
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;
const cardLayouts = buildSequentialCountdownLayout(items, 12 * scaleX, contentTop, canvas.width - 24 * scaleX, contentHeight, gap);
items.forEach((item, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
const x = 12 * scaleX + col * (cardWidth + gap);
const y = contentTop + row * (cardHeight + gap);
if (cardLayouts.length === 0) {
ctx.fillStyle = '#666666';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
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 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) {
ctx.fillStyle = '#FF0000';
ctx.font = `bold ${Math.max(14, 18 * scale)}px Arial`;
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 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.textAlign = 'center';
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 unitText = '';
@@ -1027,22 +1162,22 @@ function drawGridCountdownTemplate() {
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 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.font = `900 ${numberSize}px Arial Black`;
const centerY = y + cardHeight * 0.68;
const centerY = y + height * 0.68;
if (unitText) {
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.font = `bold ${Math.max(10, numberSize * 0.34)}px Microsoft YaHei`;
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 {
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', () => {
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();
saveCountdownState();
});