duqing2026's picture
优化
dabb305
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>交互式清单生成器 (Interactive Checklist Maker)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
[v-cloak] { display: none; }
.preview-transition { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
</style>
</head>
<body class="bg-gray-100 h-screen flex flex-col overflow-hidden text-gray-800 font-sans">
<div id="app" class="flex-1 flex flex-col h-full" v-cloak>
<!-- Header -->
<header class="bg-white border-b border-gray-200 px-4 md:px-6 py-3 flex justify-between items-center shadow-sm z-20">
<div class="flex items-center gap-3">
<div class="bg-gradient-to-br from-indigo-600 to-purple-600 text-white p-2.5 rounded-lg shadow-md transform hover:scale-105 transition duration-200">
<i class="fa-solid fa-list-check text-xl"></i>
</div>
<div>
<h1 class="text-lg md:text-xl font-bold text-gray-800 tracking-tight">交互式清单生成器</h1>
<p class="text-xs text-gray-500 font-medium">Interactive Checklist Maker</p>
</div>
</div>
<div class="flex gap-2">
<!-- Import/Export Config Buttons -->
<div class="flex bg-gray-100 p-1 rounded-lg mr-2">
<button @click="triggerImport" class="px-3 py-1.5 text-xs font-medium text-gray-600 hover:text-indigo-600 hover:bg-white rounded-md transition shadow-sm hover:shadow flex items-center" title="导入 JSON 配置">
<i class="fa-solid fa-file-import mr-1.5"></i>导入
</button>
<button @click="exportJSON" class="px-3 py-1.5 text-xs font-medium text-gray-600 hover:text-indigo-600 hover:bg-white rounded-md transition shadow-sm hover:shadow flex items-center" title="导出 JSON 配置">
<i class="fa-solid fa-file-export mr-1.5"></i>备份
</button>
</div>
<input type="file" ref="fileInput" @change="handleImport" class="hidden" accept=".json">
<button @click="resetData" class="px-3 py-1.5 text-gray-500 hover:text-red-500 hover:bg-red-50 rounded-lg transition text-sm font-medium border border-transparent hover:border-red-100">
<i class="fa-solid fa-trash-can mr-1.5"></i>清空
</button>
<button @click="exportHTML" class="px-4 md:px-6 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg shadow-lg shadow-indigo-200 transition flex items-center transform active:scale-95 text-sm md:text-base">
<i class="fa-solid fa-download mr-2"></i>导出 HTML
</button>
</div>
</header>
<!-- Main Content -->
<div class="flex-1 flex overflow-hidden">
<!-- Left Panel: Editor -->
<div class="w-1/3 min-w-[360px] max-w-[480px] bg-white border-r border-gray-200 overflow-y-auto p-4 md:p-6 flex flex-col gap-6 custom-scrollbar z-10">
<!-- Templates Selector -->
<div class="bg-indigo-50 p-4 rounded-xl border border-indigo-100">
<label class="block text-xs font-bold text-indigo-600 uppercase tracking-wider mb-2 flex items-center">
<i class="fa-solid fa-wand-magic-sparkles mr-1.5"></i>快速开始 / 模板
</label>
<select v-model="selectedTemplate" @change="applyTemplate" class="w-full px-3 py-2 bg-white border border-indigo-200 rounded-lg text-sm text-gray-700 focus:ring-2 focus:ring-indigo-500 outline-none cursor-pointer">
<option value="" disabled selected>选择一个预设模板...</option>
<option v-for="(tpl, key) in templates" :key="key" :value="key">{{ tpl.name }}</option>
</select>
</div>
<!-- Global Settings -->
<div class="space-y-4">
<div class="flex items-center justify-between pb-2 border-b border-gray-100">
<h3 class="font-bold text-gray-700 flex items-center text-sm">
<i class="fa-solid fa-sliders mr-2 text-gray-400"></i>基本信息
</h3>
</div>
<div class="space-y-3">
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">清单标题</label>
<input v-model="checklist.title" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">描述/副标题</label>
<textarea v-model="checklist.description" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition resize-none text-sm"></textarea>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">主题色系</label>
<div class="flex gap-2 flex-wrap">
<button v-for="color in themeColors" :key="color.value"
@click="checklist.theme = color.value"
:class="['w-6 h-6 rounded-full shadow-sm transition transform hover:scale-110 flex items-center justify-center ring-2 ring-offset-1', checklist.theme === color.value ? 'ring-gray-400 scale-110' : 'ring-transparent']"
:style="{ backgroundColor: color.hex }"
:title="color.label">
<i v-if="checklist.theme === color.value" class="fa-solid fa-check text-white text-[10px]"></i>
</button>
</div>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">页脚版权</label>
<input v-model="checklist.author" type="text" placeholder="例如: © 2024 Your Name" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition text-sm">
</div>
</div>
</div>
<!-- Content Editor -->
<div class="flex-1 pb-10">
<div class="flex justify-between items-center mb-4 sticky top-0 bg-white/95 backdrop-blur py-3 z-10 border-b border-gray-100">
<h3 class="font-bold text-gray-700 flex items-center text-sm">
<i class="fa-solid fa-layer-group mr-2 text-gray-400"></i>内容章节
</h3>
<button @click="addSection" class="text-indigo-600 hover:text-indigo-800 text-xs font-bold bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-full transition flex items-center">
<i class="fa-solid fa-plus mr-1"></i>添加章节
</button>
</div>
<div class="space-y-5">
<div v-for="(section, sIndex) in checklist.sections" :key="sIndex" class="border border-gray-200 rounded-xl overflow-hidden bg-white shadow-sm hover:shadow-md transition group/section">
<!-- Section Header -->
<div class="bg-gray-50 p-3 flex justify-between items-center border-b border-gray-100">
<div class="flex items-center flex-1 gap-2">
<span class="text-xs font-bold text-white bg-gray-400 rounded px-1.5 py-0.5">#{{ sIndex + 1 }}</span>
<input v-model="section.title" placeholder="输入章节标题..." class="bg-transparent font-bold text-gray-700 outline-none w-full placeholder-gray-400 text-sm">
</div>
<button @click="removeSection(sIndex)" class="text-gray-400 hover:text-red-500 p-1.5 rounded hover:bg-red-50 transition opacity-0 group-hover/section:opacity-100" title="删除章节">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
<!-- Tasks -->
<div class="p-3 space-y-2 bg-white">
<div v-for="(task, tIndex) in section.tasks" :key="tIndex" class="flex gap-2 items-start group/task p-1.5 hover:bg-gray-50 rounded-lg transition border border-transparent hover:border-gray-100 relative">
<div class="mt-2 w-1.5 h-1.5 rounded-full bg-gray-300 flex-shrink-0 group-hover/task:bg-indigo-400 transition-colors"></div>
<div class="flex-1 space-y-1 min-w-0">
<input v-model="task.text" placeholder="输入任务内容" class="w-full text-sm font-medium text-gray-700 bg-transparent border-none p-0 focus:ring-0 placeholder-gray-300">
<input v-model="task.note" placeholder="添加备注 (可选)" class="w-full text-xs text-gray-400 bg-transparent border-none p-0 focus:ring-0 placeholder-gray-200">
</div>
<button @click="removeTask(sIndex, tIndex)" class="absolute right-1 top-1 text-gray-300 hover:text-red-400 opacity-0 group-hover/task:opacity-100 transition px-1">
<i class="fa-solid fa-times"></i>
</button>
</div>
<button @click="addTask(sIndex)" class="w-full py-2 mt-2 text-xs font-medium text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 border border-dashed border-gray-200 hover:border-indigo-200 rounded-lg transition flex items-center justify-center">
<i class="fa-solid fa-plus mr-1"></i>添加任务
</button>
</div>
</div>
</div>
<div v-if="checklist.sections.length === 0" class="text-center py-12 text-gray-400 bg-gray-50 rounded-xl border-2 border-dashed border-gray-200 mt-4">
<i class="fa-regular fa-clipboard text-4xl mb-3 opacity-30"></i>
<p class="text-sm">暂无内容,请添加章节</p>
</div>
</div>
</div>
<!-- Right Panel: Live Preview -->
<div class="flex-1 bg-gray-100 flex flex-col min-w-0 relative">
<!-- Device Toolbar -->
<div class="absolute top-4 left-1/2 transform -translate-x-1/2 z-20 bg-white/90 backdrop-blur shadow-lg rounded-full p-1.5 flex gap-1 border border-gray-200">
<button @click="previewMode = 'mobile'" :class="['w-8 h-8 rounded-full flex items-center justify-center transition text-sm', previewMode === 'mobile' ? 'bg-indigo-600 text-white' : 'text-gray-500 hover:bg-gray-100']" title="手机视图">
<i class="fa-solid fa-mobile-screen"></i>
</button>
<button @click="previewMode = 'tablet'" :class="['w-8 h-8 rounded-full flex items-center justify-center transition text-sm', previewMode === 'tablet' ? 'bg-indigo-600 text-white' : 'text-gray-500 hover:bg-gray-100']" title="平板视图">
<i class="fa-solid fa-tablet-screen-button"></i>
</button>
<button @click="previewMode = 'desktop'" :class="['w-8 h-8 rounded-full flex items-center justify-center transition text-sm', previewMode === 'desktop' ? 'bg-indigo-600 text-white' : 'text-gray-500 hover:bg-gray-100']" title="桌面视图">
<i class="fa-solid fa-desktop"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto p-4 md:p-8 flex justify-center custom-scrollbar">
<div :class="['bg-white shadow-2xl overflow-hidden flex flex-col relative transition-all duration-300 ring-1 ring-black/5 preview-transition origin-top',
previewMode === 'mobile' ? 'w-[375px] rounded-[30px] my-4 min-h-[667px]' :
previewMode === 'tablet' ? 'w-[768px] rounded-[24px] my-4 min-h-[1024px]' :
'w-full max-w-3xl rounded-[16px] min-h-[800px]']">
<!-- Preview Header -->
<div :class="`bg-${checklist.theme}-600`" class="p-8 md:p-10 text-white relative overflow-hidden shrink-0">
<div class="absolute top-0 right-0 p-32 bg-white/5 rounded-full blur-3xl transform translate-x-10 -translate-y-10"></div>
<div class="relative z-10">
<h1 class="font-bold mb-3 tracking-tight break-words" :class="previewMode === 'mobile' ? 'text-2xl' : 'text-3xl'">{{ checklist.title || '我的清单' }}</h1>
<p class="opacity-90 font-light leading-relaxed break-words whitespace-pre-line" :class="previewMode === 'mobile' ? 'text-sm' : 'text-lg'">{{ checklist.description || '这里是清单的描述信息...' }}</p>
<!-- Progress Bar (Preview) -->
<div class="mt-8">
<div class="flex justify-between text-xs font-bold uppercase tracking-widest opacity-80 mb-2">
<span>进度预览</span>
<span>0%</span>
</div>
<div class="bg-black/20 rounded-full h-2.5 overflow-hidden backdrop-blur-sm">
<div class="h-full bg-white/90 w-0"></div>
</div>
</div>
</div>
</div>
<!-- Preview Content -->
<div class="p-6 md:p-10 space-y-8 flex-1 bg-white">
<div v-for="(section, index) in checklist.sections" :key="index">
<h2 class="font-bold text-gray-800 mb-4 pb-2 border-b border-gray-100 flex items-center" :class="previewMode === 'mobile' ? 'text-lg' : 'text-xl'">
<span :class="`bg-${checklist.theme}-100 text-${checklist.theme}-600`" class="w-7 h-7 rounded-lg flex items-center justify-center text-sm mr-3 font-bold shadow-sm shrink-0">
{{ index + 1 }}
</span>
<span class="break-words">{{ section.title || '未命名章节' }}</span>
</h2>
<div class="space-y-3">
<div v-for="(task, tIndex) in section.tasks" :key="tIndex"
class="flex items-start p-3 md:p-4 rounded-xl border border-gray-100 hover:border-gray-200 hover:bg-gray-50 transition cursor-pointer group">
<div :class="`text-${checklist.theme}-600`" class="mt-0.5 mr-3 md:mr-4 text-xl opacity-40 group-hover:opacity-100 transition shrink-0">
<i class="far fa-square"></i>
</div>
<div class="min-w-0">
<div class="text-gray-700 font-medium leading-snug break-words" :class="previewMode === 'mobile' ? 'text-base' : 'text-lg'">{{ task.text || '任务内容' }}</div>
<div v-if="task.note" class="text-sm text-gray-500 mt-1 leading-relaxed break-words">{{ task.note }}</div>
</div>
</div>
</div>
</div>
<div v-if="checklist.sections.length === 0" class="text-center py-20 opacity-30 select-none">
<div class="text-6xl mb-4"></div>
<p>预览区域:添加内容后在此显示</p>
</div>
</div>
<!-- Preview Footer -->
<div class="bg-gray-50 p-6 md:p-8 text-center border-t border-gray-100 shrink-0">
<p class="text-gray-400 text-xs md:text-sm font-medium">{{ checklist.author || '© 2024 Interactive Checklist' }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted } = Vue;
// 预设模板数据
const TEMPLATES = {
product_launch: {
name: "🚀 产品发布检查清单",
data: {
title: '产品发布检查清单',
description: '确保您的产品发布万无一失。从准备物料到最终上线,每一步都至关重要。',
theme: 'indigo',
author: '© 2024 Product Team',
sections: [
{
title: '准备阶段',
tasks: [
{ text: '确定发布日期', note: '建议避开重大节假日和周末' },
{ text: '准备宣传物料', note: '包括社交媒体图片、文案、演示视频等' },
{ text: '检查服务器配置', note: '确保能够承受预期的流量高峰' }
]
},
{
title: '上线检查',
tasks: [
{ text: '全功能测试', note: '最后一次回归测试' },
{ text: '更新文档', note: '确保帮助文档与最新版本一致' },
{ text: '配置监控报警', note: 'Sentry, UptimeRobot 等' }
]
}
]
}
},
travel_pack: {
name: "✈️ 旅行打包清单",
data: {
title: '旅行打包清单',
description: '再也不用担心出门忘带东西了!涵盖证件、衣物、电子产品等全方位检查。',
theme: 'emerald',
author: '© 2024 Travel Lover',
sections: [
{
title: '重要证件',
tasks: [
{ text: '护照/身份证', note: '检查有效期' },
{ text: '签证/行程单', note: '建议打印一份纸质备份' },
{ text: '酒店预订确认单', note: '' }
]
},
{
title: '电子产品',
tasks: [
{ text: '手机及充电器', note: '' },
{ text: '转换插头', note: '根据目的地选择' },
{ text: '充电宝', note: '不能托运' }
]
},
{
title: '洗漱用品',
tasks: [
{ text: '牙刷牙膏', note: '' },
{ text: '毛巾', note: '' },
{ text: '防晒霜', note: '' }
]
}
]
}
},
frontend_start: {
name: "💻 前端项目启动清单",
data: {
title: '前端项目启动清单',
description: '标准化项目初始化流程,确保代码质量和开发规范。',
theme: 'cyan',
author: '© 2024 Tech Lead',
sections: [
{
title: '环境搭建',
tasks: [
{ text: 'Git 初始化', note: 'git init' },
{ text: '配置 .gitignore', note: '排除 node_modules, .env 等' },
{ text: '锁定 Node 版本', note: '创建 .nvmrc 或 package.json engines' }
]
},
{
title: '代码规范',
tasks: [
{ text: '安装 ESLint & Prettier', note: '' },
{ text: '配置 Husky & Lint-staged', note: '提交前自动检查' },
{ text: '配置 Commitlint', note: '规范提交信息' }
]
}
]
}
},
daily_habit: {
name: "☀️ 每日晨间习惯",
data: {
title: '每日晨间习惯',
description: '开启元气满满的一天!',
theme: 'amber',
author: '© 2024 Better Me',
sections: [
{
title: '身体唤醒',
tasks: [
{ text: '喝一杯温水', note: '补充水分' },
{ text: '拉伸/瑜伽 10分钟', note: '' },
{ text: '冷水洗脸', note: '' }
]
},
{
title: '心灵充电',
tasks: [
{ text: '冥想 5 分钟', note: '' },
{ text: '阅读 10 页书', note: '' },
{ text: '写下今日最重要的3件事', note: 'To-do List' }
]
}
]
}
}
};
createApp({
setup() {
// 默认使用第一个模板
const checklist = ref(JSON.parse(JSON.stringify(TEMPLATES.product_launch.data)));
const templates = TEMPLATES;
const selectedTemplate = ref('');
const fileInput = ref(null);
const previewMode = ref('desktop');
const themeColors = [
{ value: 'indigo', hex: '#4f46e5', label: '靛蓝' },
{ value: 'blue', hex: '#2563eb', label: '深蓝' },
{ value: 'emerald', hex: '#059669', label: '翡翠' },
{ value: 'rose', hex: '#e11d48', label: '玫瑰' },
{ value: 'amber', hex: '#d97706', label: '琥珀' },
{ value: 'purple', hex: '#7c3aed', label: '紫色' },
{ value: 'cyan', hex: '#0891b2', label: '青色' },
{ value: 'slate', hex: '#475569', label: '岩灰' },
];
const totalTasks = computed(() => {
return checklist.value.sections.reduce((acc, sec) => acc + sec.tasks.length, 0);
});
const addSection = () => {
checklist.value.sections.push({
title: '',
tasks: [{ text: '', note: '' }]
});
};
const removeSection = (index) => {
checklist.value.sections.splice(index, 1);
};
const addTask = (sIndex) => {
checklist.value.sections[sIndex].tasks.push({ text: '', note: '' });
};
const removeTask = (sIndex, tIndex) => {
checklist.value.sections[sIndex].tasks.splice(tIndex, 1);
};
const resetData = () => {
if(confirm('确定要清空当前所有内容吗?')) {
checklist.value.sections = [];
checklist.value.title = '新建清单';
checklist.value.description = '';
selectedTemplate.value = '';
}
};
const applyTemplate = () => {
if (selectedTemplate.value && TEMPLATES[selectedTemplate.value]) {
if(confirm('应用模板将覆盖当前内容,确定继续吗?')) {
checklist.value = JSON.parse(JSON.stringify(TEMPLATES[selectedTemplate.value].data));
} else {
selectedTemplate.value = ''; // Revert selection
}
}
};
// JSON Import/Export
const triggerImport = () => {
fileInput.value.click();
};
const handleImport = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
// Basic validation
if (data.sections && Array.isArray(data.sections)) {
checklist.value = data;
alert('配置导入成功!');
} else {
alert('无效的配置文件格式');
}
} catch (err) {
alert('文件解析失败');
console.error(err);
}
// Reset input
event.target.value = '';
};
reader.readAsText(file);
};
const exportJSON = () => {
const dataStr = JSON.stringify(checklist.value, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${checklist.value.title || 'checklist'}_config.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const exportHTML = () => {
const data = JSON.parse(JSON.stringify(checklist.value));
if (!data.title) data.title = "Checklist";
const htmlContent = generateTemplate(data);
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${data.title.replace(/\s+/g, '_')}_checklist.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return {
checklist,
themeColors,
totalTasks,
addSection,
removeSection,
addTask,
removeTask,
resetData,
exportHTML,
templates,
selectedTemplate,
applyTemplate,
fileInput,
triggerImport,
handleImport,
exportJSON,
previewMode
};
}
}).mount('#app');
function generateTemplate(data) {
// NOTE: We must escape the closing script tag as <\/script>
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${data.title}</title>
<script src="https://cdn.tailwindcss.com"><\/script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"><\/script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
[v-cloak] { display: none; }
.slide-enter-active, .slide-leave-active { transition: all 0.3s ease; }
.slide-enter-from, .slide-leave-to { transform: translateY(-10px); opacity: 0; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.confetti { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999; }
</style>
</head>
<body class="bg-gray-50 min-h-screen text-gray-800 font-sans selection:bg-${data.theme}-100 selection:text-${data.theme}-800">
<div id="app" class="max-w-3xl mx-auto min-h-screen bg-white shadow-2xl flex flex-col relative" v-cloak>
<!-- Sticky Header -->
<header class="bg-${data.theme}-600 text-white p-8 sticky top-0 z-30 shadow-lg transition-all duration-300" :class="{'py-4 px-6': scrolled, 'p-8 md:p-12': !scrolled}">
<div class="flex justify-between items-start">
<div class="flex-1 pr-4">
<h1 class="font-bold transition-all duration-300 leading-tight" :class="scrolled ? 'text-xl' : 'text-3xl md:text-4xl mb-3'">${data.title}</h1>
<p class="opacity-90 text-sm md:text-base font-light leading-relaxed transition-all duration-300 whitespace-pre-line" v-show="!scrolled" :style="{ maxHeight: scrolled ? '0px' : '200px', opacity: scrolled ? 0 : 0.9 }">${data.description}</p>
</div>
<div class="text-right flex-shrink-0 bg-white/10 p-2 rounded-lg backdrop-blur-sm border border-white/10">
<div class="text-2xl font-bold">{{ progress }}%</div>
<div class="text-xs opacity-80 font-mono">{{ completedCount }} / {{ totalCount }}</div>
</div>
</div>
<!-- Progress Bar -->
<div class="mt-6 bg-black/20 rounded-full h-2.5 overflow-hidden backdrop-blur-sm shadow-inner" :class="{'mt-3 h-1.5': scrolled}">
<div class="h-full bg-white/90 transition-all duration-700 ease-out shadow-sm" :style="{ width: progress + '%' }"></div>
</div>
</header>
<!-- Main List -->
<main class="flex-1 p-6 md:p-10 space-y-10">
<div v-for="(section, sIndex) in sections" :key="sIndex" class="animate-fade-in" :style="{ animationDelay: sIndex * 100 + 'ms' }">
<h2 class="text-xl font-bold mb-5 pb-3 border-b border-gray-100 flex items-center text-gray-800 sticky top-[80px] bg-white/95 backdrop-blur z-20 pt-2 -mx-2 px-2">
<span class="w-8 h-8 rounded-lg bg-${data.theme}-100 text-${data.theme}-600 flex items-center justify-center text-sm mr-3 font-bold shadow-sm">
{{ sIndex + 1 }}
</span>
{{ section.title }}
</h2>
<div class="space-y-3">
<div v-for="(task, tIndex) in section.tasks" :key="tIndex"
@click="toggleTask(sIndex, tIndex)"
class="flex items-start p-4 rounded-xl border transition-all duration-200 cursor-pointer select-none group relative overflow-hidden"
:class="task.checked ? 'bg-${data.theme}-50 border-${data.theme}-200 shadow-inner' : 'bg-white border-gray-100 hover:border-gray-300 hover:bg-gray-50 hover:shadow-md hover:-translate-y-0.5'">
<!-- Checkbox Icon -->
<div class="mt-1 mr-4 text-2xl transition-all duration-300 transform"
:class="task.checked ? 'text-${data.theme}-600 scale-110' : 'text-gray-300 group-hover:text-${data.theme}-400 group-hover:scale-110'">
<i :class="task.checked ? 'fa-solid fa-square-check' : 'fa-regular fa-square'"></i>
</div>
<div class="flex-1 z-10 min-w-0">
<div class="font-medium text-lg leading-snug transition-all duration-300 break-words"
:class="task.checked ? 'text-${data.theme}-800 line-through opacity-60 decoration-2 decoration-${data.theme}-300' : 'text-gray-700'">
{{ task.text }}
</div>
<div v-if="task.note" class="text-sm mt-1.5 transition-opacity duration-300 leading-relaxed break-words"
:class="task.checked ? 'text-${data.theme}-600 opacity-50' : 'text-gray-500'">
{{ task.note }}
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="p-8 text-center text-gray-400 text-sm border-t border-gray-100 bg-gray-50">
<div class="flex justify-center gap-6 mb-4">
<button @click="resetProgress" class="text-gray-400 hover:text-red-500 hover:underline text-xs flex items-center transition">
<i class="fa-solid fa-rotate-left mr-1"></i>重置进度
</button>
</div>
<p>${data.author || 'Generated by Interactive Checklist Maker'}</p>
</footer>
<!-- Confetti Canvas -->
<canvas id="confetti" class="confetti"></canvas>
</div>
<script>
const { createApp, ref, computed, onMounted, watch } = Vue;
// Use a unique key based on title hash to allow multiple checklists
const STORAGE_KEY = 'checklist_${btoa(unescape(encodeURIComponent(data.title))).replace(/[^a-zA-Z0-9]/g, '')}_v1';
createApp({
setup() {
const rawSections = ${JSON.stringify(data.sections)};
const sections = ref(rawSections.map(s => ({
...s,
tasks: s.tasks.map(t => ({ ...t, checked: false }))
})));
const scrolled = ref(false);
let confettiCtx = null;
onMounted(() => {
// Load state
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
const savedState = JSON.parse(saved);
sections.value.forEach((sec, sIdx) => {
if (savedState[sIdx]) {
sec.tasks.forEach((task, tIdx) => {
if (savedState[sIdx][tIdx] !== undefined) {
task.checked = savedState[sIdx][tIdx];
}
});
}
});
} catch (e) { console.error('Load failed', e); }
}
window.addEventListener('scroll', () => {
scrolled.value = window.scrollY > 50;
});
// Init confetti
const canvas = document.getElementById('confetti');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
confettiCtx = canvas.getContext('2d');
});
watch(sections, (newVal) => {
const state = newVal.map(s => s.tasks.map(t => t.checked));
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
// Check for completion
if (progress.value === 100) {
triggerConfetti();
}
}, { deep: true });
const totalCount = computed(() => {
return sections.value.reduce((acc, s) => acc + s.tasks.length, 0);
});
const completedCount = computed(() => {
return sections.value.reduce((acc, s) => acc + s.tasks.filter(t => t.checked).length, 0);
});
const progress = computed(() => {
if (totalCount.value === 0) return 0;
return Math.round((completedCount.value / totalCount.value) * 100);
});
const toggleTask = (sIdx, tIdx) => {
sections.value[sIdx].tasks[tIdx].checked = !sections.value[sIdx].tasks[tIdx].checked;
if (navigator.vibrate && sections.value[sIdx].tasks[tIdx].checked) {
navigator.vibrate(10);
}
};
const resetProgress = () => {
if(confirm('确定要清空当前进度吗?')) {
sections.value.forEach(s => s.tasks.forEach(t => t.checked = false));
localStorage.removeItem(STORAGE_KEY);
}
};
// Simple Confetti Implementation
const triggerConfetti = () => {
const colors = ['#${data.theme === 'indigo' ? '4f46e5' : '10b981'}', '#fbbf24', '#ef4444', '#3b82f6'];
let particles = [];
for(let i=0; i<100; i++) {
particles.push({
x: Math.random() * window.innerWidth,
y: -20,
vx: Math.random() * 4 - 2,
vy: Math.random() * 4 + 2,
color: colors[Math.floor(Math.random() * colors.length)],
size: Math.random() * 5 + 2
});
}
const animate = () => {
if(particles.length === 0) return;
confettiCtx.clearRect(0, 0, window.innerWidth, window.innerHeight);
particles.forEach((p, i) => {
p.x += p.vx;
p.y += p.vy;
confettiCtx.fillStyle = p.color;
confettiCtx.fillRect(p.x, p.y, p.size, p.size);
if(p.y > window.innerHeight) particles.splice(i, 1);
});
requestAnimationFrame(animate);
};
animate();
};
return {
sections,
toggleTask,
progress,
totalCount,
completedCount,
resetProgress,
scrolled
};
}
}).mount('#app');
<\/script>
</body>
</html>`;
}
</script>
</body>
</html>