Files
Letters/gen/index.html
T
biss f956f53cf7
Vercel Deploy / deploy (push) Failing after 2m39s
暂借lettes服务
2026-04-16 20:14:01 +08:00

312 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>墨水屏倒计时 Pro - 阵列卡片版</title>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<style>
:root { --ep-black: #000000; --ep-red: #ff0000; --ep-white: #ffffff; }
body { font-family: "PingFang SC", "STHeiti", sans-serif; background: #f0f2f5; display: flex; flex-direction: column; align-items: center; padding: 20px; gap: 20px; }
/* --- 4.2寸预览区 (400x300) --- */
#capture-area {
width: 400px; height: 300px;
background: var(--ep-white);
border: 1px solid #000;
display: flex; flex-direction: column;
box-sizing: border-box; overflow: hidden;
position: relative;
}
.top-date {
position: absolute;
top: 8px; right: 12px;
font-size: 10px;
color: var(--ep-black);
font-weight: bold;
text-align: right;
line-height: 1.2;
z-index: 10;
}
.header {
text-align: left;
padding: 15px 0 10px 20px;
flex-shrink: 0;
}
#title-text {
color: var(--ep-black);
border-left: 5px solid var(--ep-red);
display: inline-block;
padding: 0 10px;
font-weight: 900;
margin: 0;
font-size: 20px;
}
/* --- 阵列布局容器 --- */
.exam-grid-preview {
padding: 0 15px 15px 15px;
flex-grow: 1;
display: grid;
/* 默认两列排布 */
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 1fr;
gap: 10px;
}
/* --- 矩形卡片样式 --- */
.preview-card {
border: 2px solid var(--ep-black);
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 8px;
position: relative;
background: white;
text-align: center;
}
/* 重要考试:红色边框 + 填充效果 */
.is-important-card {
border: 3px solid var(--ep-red) !important;
}
.is-important-card::after {
content: "★";
position: absolute;
top: 2px; right: 4px;
color: var(--ep-red);
font-size: 14px;
}
.preview-name {
font-weight: bold;
color: var(--ep-black);
font-size: 14px;
margin-bottom: 4px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-days-val {
font-weight: 900;
color: var(--ep-red);
font-family: 'Arial Black', sans-serif;
font-size: 32px;
line-height: 1;
}
.preview-unit {
font-size: 12px;
color: var(--ep-black);
margin-left: 2px;
}
/* --- 管理区 --- */
.admin-panel { width: 520px; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
.input-card { display: flex; gap: 8px; margin-bottom: 15px; align-items: center; background: #f8f9fa; padding: 12px; border-radius: 8px; }
.input-card input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
#sortable-list { list-style: none; padding: 0; margin: 0; }
.manage-item {
display: flex; align-items: center; gap: 12px;
background: #fff; border: 1px solid #eee; margin-bottom: 8px;
padding: 10px; border-radius: 6px; cursor: move;
}
.drag-handle { color: #ccc; cursor: grab; font-size: 20px; user-select: none; }
.manage-info { flex-grow: 1; display: flex; gap: 8px; }
.manage-info input[type="text"] { flex: 2; }
.btn-del { background: #ff4d4f; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; }
.action-btns { display: flex; gap: 10px; margin-top: 20px; }
.btn-main { flex: 1; padding: 12px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; }
.btn-save { background: var(--ep-red); color: white; }
.btn-data { background: #555; color: white; }
</style>
</head>
<body>
<div id="capture-area">
<div class="top-date" id="live-date"></div>
<div class="header">
<h1 id="title-text">考试倒计时</h1>
</div>
<div class="exam-grid-preview" id="preview-container"></div>
</div>
<div class="admin-panel">
<h3 style="margin-top:0; border-left: 4px solid var(--ep-red); padding-left: 10px;">项目管理</h3>
<div class="input-card">
<input type="text" id="name-in" placeholder="项目名称">
<input type="date" id="date-in">
<label style="font-size:13px; cursor:pointer"><input type="checkbox" id="imp-in"> 重要</label>
<button onclick="addItem()" style="background:#000; color:white; border:none; padding:8px 15px; border-radius:4px; cursor:pointer">添加</button>
</div>
<ul id="sortable-list"></ul>
<div class="action-btns">
<button class="btn-main btn-data" onclick="exportJSON()">备份</button>
<button class="btn-main btn-data" onclick="document.getElementById('file-in').click()">恢复</button>
<input type="file" id="file-in" style="display:none" onchange="importJSON(this)">
<button class="btn-main btn-save" onclick="downloadImage()">保存 400x300 图片</button>
</div>
</div>
<script>
const getTodayStr = () => new Date().toISOString().split('T')[0];
// 默认数据
let exams = JSON.parse(localStorage.getItem('dragExamGridV1')) || [
{ name: "期末考试", date: "2026-06-20", imp: true },
{ name: "英语四级", date: "2026-06-15", imp: false },
{ name: "驾照科目一", date: "2026-05-10", imp: false },
{ name: "健身计划", date: getTodayStr(), imp: false }
];
function updateLiveDate() {
const now = new Date();
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
document.getElementById('live-date').innerHTML = `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}日<br>${weekDays[now.getDay()]}`;
}
Sortable.create(document.getElementById('sortable-list'), {
animation: 150, handle: '.drag-handle', onEnd: () => saveOrder()
});
function calculateDays(targetDate) {
const today = new Date().setHours(0,0,0,0);
const target = new Date(targetDate).setHours(0,0,0,0);
const diff = target - today;
return Math.ceil(diff / (1000 * 60 * 60 * 24));
}
function renderAll() {
renderPreview();
renderManageList();
updateLiveDate();
localStorage.setItem('dragExamGridV1', JSON.stringify(exams));
}
function renderPreview() {
const container = document.getElementById('preview-container');
container.innerHTML = '';
const count = exams.length;
// 根据数量动态调整列数
if (count <= 1) {
container.style.gridTemplateColumns = "1fr";
} else if (count > 4) {
container.style.gridTemplateColumns = "repeat(3, 1fr)";
} else {
container.style.gridTemplateColumns = "repeat(2, 1fr)";
}
exams.forEach(item => {
const days = calculateDays(item.date);
let dayHTML = '';
if (days === 0) {
dayHTML = `<span class="preview-days-val" style="font-size:24px">今天</span>`;
} else if (days < 0) {
dayHTML = `<span class="preview-days-val" style="font-size:24px; color:#666">已过</span>`;
} else {
dayHTML = `<span class="preview-days-val">${days}</span><span class="preview-unit">天</span>`;
}
container.innerHTML += `
<div class="preview-card ${item.imp ? 'is-important-card' : ''}">
<div class="preview-name">${item.name}</div>
<div class="preview-days-info">
${dayHTML}
</div>
</div>
`;
});
}
function renderManageList() {
const list = document.getElementById('sortable-list');
list.innerHTML = '';
exams.forEach((item, index) => {
list.innerHTML += `
<li class="manage-item" data-index="${index}">
<span class="drag-handle">☰</span>
<div class="manage-info">
<input type="text" value="${item.name}" onchange="updateItem(${index}, 'name', this.value)">
<input type="date" value="${item.date}" onchange="updateItem(${index}, 'date', this.value)">
<label><input type="checkbox" ${item.imp?'checked':''} onchange="updateItem(${index}, 'imp', this.checked)"> 重要</label>
</div>
<button class="btn-del" onclick="removeItem(${index})">×</button>
</li>
`;
});
}
function updateItem(index, key, value) {
exams[index][key] = value;
renderPreview();
localStorage.setItem('dragExamGridV1', JSON.stringify(exams));
}
function saveOrder() {
const newExams = [];
document.querySelectorAll('.manage-item').forEach(el => {
newExams.push(exams[el.getAttribute('data-index')]);
});
exams = newExams;
renderAll();
}
function addItem() {
const n = document.getElementById('name-in').value;
const d = document.getElementById('date-in').value;
const i = document.getElementById('imp-in').checked;
if(n && d) {
exams.push({name:n, date:d, imp:i});
renderAll();
document.getElementById('name-in').value = '';
}
}
function removeItem(index) {
exams.splice(index, 1);
renderAll();
}
function downloadImage() {
html2canvas(document.getElementById('capture-area'), {
width: 400,
height: 300,
scale: 2,
backgroundColor: "#ffffff"
})
.then(canvas => {
const link = document.createElement('a');
link.download = `E-Paper_Grid_${getTodayStr()}.png`;
link.href = canvas.toDataURL("image/png");
link.click();
});
}
function exportJSON() {
const blob = new Blob([JSON.stringify(exams)], {type: 'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob); a.download = 'exams_config.json'; a.click();
}
function importJSON(input) {
const reader = new FileReader();
reader.onload = e => { exams = JSON.parse(e.target.result); renderAll(); };
reader.readAsText(input.files[0]);
}
renderAll();
</script>
</body>
</html>