@@ -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;
|
||||
|
||||
+168
-33
@@ -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 cardLayouts = buildSequentialCountdownLayout(items, 12 * scaleX, contentTop, canvas.width - 24 * scaleX, contentHeight, 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;
|
||||
|
||||
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);
|
||||
const days = getCountdownDays(item.date);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user