interactive-checklist-pro / templates /export_template.html
duqing2026's picture
优化
79f5a9b
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ checklist_data.title | default('交互式清单') }}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.checkbox-wrapper input:checked + div {
background-color: currentColor;
border-color: currentColor;
}
.checkbox-wrapper input:checked + div svg {
display: block;
}
/* Smooth transitions */
.transition-all-300 { transition: all 0.3s ease; }
</style>
</head>
<body class="bg-gray-50 min-h-screen pb-20">
<!-- Data Injection -->
<script>
const checklistData = {{ checklist_data | tojson }};
const STORAGE_KEY = 'checklist_pro_' + btoa(encodeURIComponent(checklistData.title || 'default')).slice(0, 16);
</script>
<div id="app" class="max-w-md mx-auto bg-white min-h-screen shadow-2xl relative">
<!-- Header -->
<header id="header-bg" class="text-white p-8 pt-12 rounded-b-[2rem] shadow-lg relative overflow-hidden transition-colors duration-300">
<div class="relative z-10">
<div class="text-xs opacity-80 mb-2 font-medium tracking-wider uppercase">{{ checklist_data.author | default('') }}</div>
<h1 class="text-3xl font-bold leading-tight mb-3">{{ checklist_data.title | default('Checklist') }}</h1>
<p class="text-white/90 text-sm leading-relaxed">{{ checklist_data.description | default('') }}</p>
<!-- Progress -->
<div class="mt-8">
<div class="flex justify-between text-xs font-bold mb-2 opacity-90">
<span id="progress-text">0% 完成</span>
<span id="count-text">0/0</span>
</div>
<div class="h-2.5 bg-black/20 rounded-full overflow-hidden backdrop-blur-sm">
<div id="progress-bar" class="h-full bg-white/95 w-0 transition-all duration-500 ease-out rounded-full"></div>
</div>
</div>
</div>
<!-- Decorative Circles -->
<div class="absolute top-0 right-0 -mr-10 -mt-10 w-40 h-40 bg-white/10 rounded-full blur-2xl"></div>
<div class="absolute bottom-0 left-0 -ml-10 -mb-5 w-32 h-32 bg-black/10 rounded-full blur-xl"></div>
</header>
<!-- Content -->
<main class="p-6 space-y-8">
<div id="groups-container" class="space-y-8">
<!-- Groups will be rendered here -->
</div>
<!-- Reset Button -->
<div class="pt-8 pb-4 text-center">
<button onclick="resetProgress()" class="text-xs text-gray-400 hover:text-gray-600 underline decoration-dotted">
重置进度 (Clear Progress)
</button>
</div>
</main>
<!-- Footer -->
<footer class="text-center p-6 text-xs text-gray-300 border-t border-gray-50">
Created with Interactive Checklist Pro
</footer>
</div>
<!-- Logic -->
<script>
// Theme Mapping
const themeMap = {
'indigo': 'bg-indigo-600',
'blue': 'bg-blue-600',
'emerald': 'bg-emerald-600',
'rose': 'bg-rose-600',
'amber': 'bg-amber-600',
'slate': 'bg-slate-800',
};
const textThemeMap = {
'indigo': 'text-indigo-600',
'blue': 'text-blue-600',
'emerald': 'text-emerald-600',
'rose': 'text-rose-600',
'amber': 'text-amber-600',
'slate': 'text-slate-800',
};
const themeClass = themeMap[checklistData.theme] || 'bg-indigo-600';
const textClass = textThemeMap[checklistData.theme] || 'text-indigo-600';
// Apply Theme
document.getElementById('header-bg').classList.add(themeClass);
// State
let state = {
checkedItems: [] // array of item IDs (groupIndex-itemIndex)
};
// Load State
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
state = JSON.parse(saved);
} catch(e) { console.error('Load failed', e); }
}
// Render
const container = document.getElementById('groups-container');
let totalItemsCount = 0;
checklistData.groups.forEach((group, gIndex) => {
const groupEl = document.createElement('div');
// Group Title
const titleHTML = `
<h2 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
<span class="w-1.5 h-5 rounded-full ${themeClass}"></span>
${group.title}
</h2>
`;
// Items
let itemsHTML = '<div class="space-y-3">';
group.items.forEach((item, iIndex) => {
totalItemsCount++;
const itemId = `${gIndex}-${iIndex}`;
const isChecked = state.checkedItems.includes(itemId);
itemsHTML += `
<label class="checkbox-wrapper flex items-start gap-3 p-3 rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm bg-white transition-all-300 cursor-pointer select-none group ${isChecked ? 'bg-gray-50/50' : ''}">
<input type="checkbox" class="hidden"
onchange="toggleItem('${itemId}')"
${isChecked ? 'checked' : ''}>
<div class="w-6 h-6 rounded-lg border-2 border-gray-200 flex items-center justify-center text-white shrink-0 transition-colors ${textClass.replace('text-', 'text-')} group-hover:border-gray-300">
<svg class="w-4 h-4 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
</div>
<div class="flex-1 pt-0.5 ${isChecked ? 'opacity-50 line-through grayscale' : ''} transition-all duration-300">
<div class="text-gray-700 font-medium text-sm leading-snug">${item.text}</div>
${item.note ? `<div class="text-xs text-gray-500 mt-1">${item.note}</div>` : ''}
${item.link ? `<a href="${item.link}" target="_blank" class="inline-flex items-center gap-1 text-xs mt-1.5 ${textClass} hover:underline" onclick="event.stopPropagation()"><i class="ph ph-link"></i> Resource</a>` : ''}
</div>
</label>
`;
});
itemsHTML += '</div>';
groupEl.innerHTML = titleHTML + itemsHTML;
container.appendChild(groupEl);
});
// Update UI
function updateUI() {
const checkedCount = state.checkedItems.length;
const percentage = totalItemsCount === 0 ? 0 : Math.round((checkedCount / totalItemsCount) * 100);
document.getElementById('progress-bar').style.width = `${percentage}%`;
document.getElementById('progress-text').innerText = `${percentage}% 完成`;
document.getElementById('count-text').innerText = `${checkedCount}/${totalItemsCount}`;
// Check for completion
if (percentage === 100 && totalItemsCount > 0) {
triggerConfetti();
}
}
// Logic
window.toggleItem = (id) => {
if (state.checkedItems.includes(id)) {
state.checkedItems = state.checkedItems.filter(i => i !== id);
} else {
state.checkedItems.push(id);
}
save();
// Re-render specifically this item or just toggle class (Optimization: toggle class directly)
// For simplicity in this vanilla script, reloading page or complex DOM manipulation is overkill.
// But we need to update the visual state of the specific element immediately.
// The input 'checked' state handles the icon. We need to handle the strikethrough class.
const input = document.querySelector(`input[onchange="toggleItem('${id}')"]`);
const wrapper = input.closest('label');
const contentDiv = wrapper.querySelector('.flex-1');
if (state.checkedItems.includes(id)) {
contentDiv.classList.add('opacity-50', 'line-through', 'grayscale');
wrapper.classList.add('bg-gray-50/50');
} else {
contentDiv.classList.remove('opacity-50', 'line-through', 'grayscale');
wrapper.classList.remove('bg-gray-50/50');
}
updateUI();
};
function save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
window.resetProgress = () => {
if(confirm('确定要清空所有进度吗?')) {
state.checkedItems = [];
save();
location.reload();
}
};
function triggerConfetti() {
const count = 200;
const defaults = {
origin: { y: 0.7 }
};
function fire(particleRatio, opts) {
confetti(Object.assign({}, defaults, opts, {
particleCount: Math.floor(count * particleRatio)
}));
}
fire(0.25, { spread: 26, startVelocity: 55 });
fire(0.2, { spread: 60 });
fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 });
fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 });
fire(0.1, { spread: 120, startVelocity: 45 });
}
// Init
// We need to apply initial visual states (strikethrough) since we static rendered
// Actually, we should iterate and apply classes based on state on load.
state.checkedItems.forEach(id => {
const input = document.querySelector(`input[onchange="toggleItem('${id}')"]`);
if(input) {
input.checked = true;
const wrapper = input.closest('label');
const contentDiv = wrapper.querySelector('.flex-1');
contentDiv.classList.add('opacity-50', 'line-through', 'grayscale');
wrapper.classList.add('bg-gray-50/50');
}
});
updateUI();
</script>
</body>
</html>