From 923525b6d1de4968eb0d4f5ad99120816593ead7 Mon Sep 17 00:00:00 2001 From: biss Date: Sun, 26 Apr 2026 19:30:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=80=92=E8=AE=A1=E6=97=B6?= =?UTF-8?q?=E6=8E=92=E7=89=88=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gen/css/main.css | 28 +++++++ gen/js/main.js | 199 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 195 insertions(+), 32 deletions(-) diff --git a/gen/css/main.css b/gen/css/main.css index 18b19dd..b2b32ad 100644 --- a/gen/css/main.css +++ b/gen/css/main.css @@ -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; diff --git a/gen/js/main.js b/gen/js/main.js index 9d86c02..a52428f 100644 --- a/gen/js/main.js +++ b/gen/js/main.js @@ -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 = ` + + `; + 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(); });