Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>交互式清单专家 (Interactive Checklist Pro)</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <!-- Phosphor Icons --> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></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; } | |
| .scrollbar-hide::-webkit-scrollbar { display: none; } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 text-gray-800 h-screen flex flex-col overflow-hidden"> | |
| <div id="app" class="flex-1 flex flex-col h-full"> | |
| {% raw %} | |
| <!-- Header --> | |
| <header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0 z-10"> | |
| <div class="flex items-center gap-3"> | |
| <div class="bg-indigo-600 text-white p-2 rounded-lg"> | |
| <i class="ph ph-check-square text-xl"></i> | |
| </div> | |
| <h1 class="text-xl font-bold text-gray-900 tracking-tight">Interactive Checklist Pro</h1> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <div class="flex items-center gap-1 mr-2 border-r border-gray-200 pr-3"> | |
| <button @click="importJSON" class="text-gray-500 hover:text-indigo-600 p-2 rounded-lg transition" title="导入配置 (Import JSON)"> | |
| <i class="ph ph-upload-simple text-lg"></i> | |
| </button> | |
| <button @click="exportJSON" class="text-gray-500 hover:text-indigo-600 p-2 rounded-lg transition" title="备份配置 (Export JSON)"> | |
| <i class="ph ph-floppy-disk text-lg"></i> | |
| </button> | |
| <button @click="clearStorage" class="text-gray-400 hover:text-red-600 p-2 rounded-lg transition" title="重置 (Reset)"> | |
| <i class="ph ph-arrow-counter-clockwise text-lg"></i> | |
| </button> | |
| </div> | |
| <button @click="downloadHTML" :disabled="isExporting" class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-5 py-2 rounded-lg font-medium transition-colors shadow-sm disabled:opacity-50"> | |
| <i class="ph ph-download-simple" v-if="!isExporting"></i> | |
| <i class="ph ph-spinner animate-spin" v-else></i> | |
| <span v-text="isExporting ? '生成中...' : '导出单文件 HTML'"></span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex overflow-hidden"> | |
| <!-- Left: Editor --> | |
| <div class="w-1/2 bg-white border-r border-gray-200 flex flex-col overflow-hidden"> | |
| <div class="p-6 overflow-y-auto custom-scrollbar flex-1 pb-20"> | |
| <!-- Global Settings --> | |
| <section class="mb-8"> | |
| <h2 class="text-sm uppercase tracking-wider text-gray-500 font-semibold mb-4">基本信息</h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">清单标题</label> | |
| <input v-model="checklist.title" type="text" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition" placeholder="例如:SEO 终极检查表"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">描述 / 引言</label> | |
| <textarea v-model="checklist.description" rows="3" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition" placeholder="简要介绍这个清单的用途..."></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">作者 / 品牌</label> | |
| <input v-model="checklist.author" type="text" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition" placeholder="例如:YourName @ Twitter"> | |
| </div> | |
| <!-- Theme Selection --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">主题色</label> | |
| <div class="flex gap-3"> | |
| <button v-for="color in themeColors" :key="color.value" | |
| @click="checklist.theme = color.value" | |
| class="w-8 h-8 rounded-full border-2 transition-all" | |
| :class="[color.bgClass, checklist.theme === color.value ? 'border-gray-900 scale-110' : 'border-transparent opacity-70 hover:opacity-100']"> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Content Editor --> | |
| <section> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-sm uppercase tracking-wider text-gray-500 font-semibold">清单内容</h2> | |
| <button @click="addGroup" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium flex items-center gap-1"> | |
| <i class="ph ph-plus"></i> 添加分组 | |
| </button> | |
| </div> | |
| <div class="space-y-6"> | |
| <div v-for="(group, gIndex) in checklist.groups" :key="gIndex" class="border border-gray-200 rounded-xl bg-gray-50 overflow-hidden"> | |
| <!-- Group Header --> | |
| <div class="bg-gray-100 p-4 border-b border-gray-200 flex items-start gap-3"> | |
| <div class="flex flex-col gap-1 pt-1"> | |
| <button @click="moveGroup(gIndex, -1)" :disabled="gIndex === 0" class="text-gray-400 hover:text-gray-700 disabled:opacity-30"><i class="ph ph-caret-up"></i></button> | |
| <button @click="moveGroup(gIndex, 1)" :disabled="gIndex === checklist.groups.length - 1" class="text-gray-400 hover:text-gray-700 disabled:opacity-30"><i class="ph ph-caret-down"></i></button> | |
| </div> | |
| <div class="flex-1"> | |
| <input v-model="group.title" type="text" class="w-full bg-transparent border-b border-transparent hover:border-gray-300 focus:border-indigo-500 focus:ring-0 px-0 py-1 font-semibold text-gray-800 placeholder-gray-400 transition" placeholder="分组名称 (例如:准备工作)"> | |
| </div> | |
| <button @click="removeGroup(gIndex)" class="text-red-400 hover:text-red-600 p-1"><i class="ph ph-trash"></i></button> | |
| </div> | |
| <!-- Items List --> | |
| <div class="p-4 space-y-3"> | |
| <div v-for="(item, iIndex) in group.items" :key="iIndex" class="flex items-start gap-3 bg-white p-3 rounded-lg border border-gray-100 shadow-sm group"> | |
| <div class="flex flex-col gap-0.5 pt-1 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <button @click="moveItem(gIndex, iIndex, -1)" :disabled="iIndex === 0" class="text-gray-300 hover:text-gray-600 text-xs"><i class="ph ph-caret-up"></i></button> | |
| <button @click="moveItem(gIndex, iIndex, 1)" :disabled="iIndex === group.items.length - 1" class="text-gray-300 hover:text-gray-600 text-xs"><i class="ph ph-caret-down"></i></button> | |
| </div> | |
| <div class="flex-1 space-y-2"> | |
| <input v-model="item.text" type="text" class="w-full border-b border-gray-100 focus:border-indigo-300 focus:ring-0 px-0 py-0.5 text-sm text-gray-800 placeholder-gray-400" placeholder="检查项内容..."> | |
| <!-- Optional Fields Toggle --> | |
| <div class="flex gap-4 text-xs"> | |
| <input v-model="item.note" type="text" class="flex-1 bg-gray-50 rounded px-2 py-1 text-gray-600 placeholder-gray-300 border-none" placeholder="备注/提示 (可选)"> | |
| <input v-model="item.link" type="text" class="flex-1 bg-gray-50 rounded px-2 py-1 text-gray-600 placeholder-gray-300 border-none" placeholder="链接 URL (可选)"> | |
| </div> | |
| </div> | |
| <button @click="removeItem(gIndex, iIndex)" class="text-gray-300 hover:text-red-500 pt-1 opacity-0 group-hover:opacity-100 transition-opacity"><i class="ph ph-x"></i></button> | |
| </div> | |
| <button @click="addItem(gIndex)" class="w-full py-2 border border-dashed border-gray-300 rounded-lg text-gray-500 text-sm hover:border-indigo-400 hover:text-indigo-600 transition flex items-center justify-center gap-2"> | |
| <i class="ph ph-plus-circle"></i> 添加检查项 | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </div> | |
| <!-- Right: Preview --> | |
| <div class="w-1/2 bg-gray-100 flex flex-col overflow-hidden relative"> | |
| <div class="absolute top-4 right-4 bg-white/80 backdrop-blur px-3 py-1 rounded-full text-xs font-medium text-gray-500 border border-gray-200 shadow-sm z-10"> | |
| 实时预览 (Export Preview) | |
| </div> | |
| <div class="flex-1 overflow-y-auto p-8 flex justify-center"> | |
| <!-- Phone/Tablet Frame --> | |
| <div class="w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden border-8 border-gray-800 flex flex-col min-h-[600px] max-h-[90vh]"> | |
| <!-- App Header --> | |
| <div class="p-6 text-white transition-colors duration-300 shrink-0" :class="currentThemeClass"> | |
| <div class="text-xs opacity-80 mb-1 uppercase tracking-wider">{{ checklist.author || 'Author Name' }}</div> | |
| <h2 class="text-2xl font-bold leading-tight">{{ checklist.title || 'Checklist Title' }}</h2> | |
| <p class="text-sm opacity-90 mt-2 line-clamp-2" v-if="checklist.description">{{ checklist.description }}</p> | |
| <!-- Progress Bar Preview --> | |
| <div class="mt-6"> | |
| <div class="flex justify-between text-xs font-medium mb-1 opacity-90"> | |
| <span>进度 0%</span> | |
| <span>0/{{ totalItems }}</span> | |
| </div> | |
| <div class="h-2 bg-black/20 rounded-full overflow-hidden"> | |
| <div class="h-full bg-white/90 w-0"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- App Content --> | |
| <div class="flex-1 overflow-y-auto p-4 space-y-6 bg-white"> | |
| <div v-for="(group, gIndex) in checklist.groups" :key="gIndex"> | |
| <h3 class="font-bold text-gray-800 mb-3 flex items-center gap-2"> | |
| <span class="w-1.5 h-4 rounded-full" :class="currentThemeTextClass.replace('text-', 'bg-')"></span> | |
| {{ group.title || 'Untitled Group' }} | |
| </h3> | |
| <div class="space-y-2"> | |
| <div v-for="(item, iIndex) in group.items" :key="iIndex" class="flex items-start gap-3 p-3 rounded-lg border border-gray-100 hover:border-gray-200 transition-colors cursor-pointer"> | |
| <div class="w-5 h-5 rounded border-2 border-gray-300 shrink-0 mt-0.5"></div> | |
| <div class="flex-1"> | |
| <div class="text-gray-700 text-sm font-medium">{{ item.text || 'Item text' }}</div> | |
| <div v-if="item.note" class="text-xs text-gray-500 mt-1">{{ item.note }}</div> | |
| <div v-if="item.link" class="text-xs mt-1 text-blue-500 underline truncate">Resource Link</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-if="checklist.groups.length === 0" class="text-center text-gray-400 py-10 text-sm"> | |
| 暂无内容,请在左侧添加 | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <div class="bg-gray-50 p-3 text-center text-xs text-gray-400 border-t border-gray-100 shrink-0"> | |
| Powered by Interactive Checklist Pro | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endraw %} | |
| <script> | |
| const { createApp, ref, computed, watch, onMounted } = Vue; | |
| createApp({ | |
| setup() { | |
| const checklist = ref({ | |
| title: '产品发布检查清单', | |
| description: '确保您的产品发布顺利无误的终极指南。涵盖从准备到发布后的关键步骤。', | |
| author: 'Product Team', | |
| theme: 'indigo', | |
| groups: [ | |
| { | |
| title: '前期准备', | |
| items: [ | |
| { text: '确定目标受众', note: '分析用户画像和痛点', link: '' }, | |
| { text: '完成核心功能测试', note: '确保无重大 Bug', link: '' }, | |
| { text: '准备营销素材', note: '包括 Banner、文案、演示视频', link: '' } | |
| ] | |
| }, | |
| { | |
| title: '发布日', | |
| items: [ | |
| { text: '部署上线', note: '执行部署脚本,检查服务状态', link: '' }, | |
| { text: '发布公告', note: '社交媒体、邮件列表、博客', link: '' }, | |
| { text: '监控系统', note: '关注服务器负载和错误日志', link: '' } | |
| ] | |
| } | |
| ] | |
| }); | |
| // Auto-save logic | |
| onMounted(() => { | |
| const saved = localStorage.getItem('checklist_pro_draft'); | |
| if (saved) { | |
| try { | |
| checklist.value = JSON.parse(saved); | |
| } catch (e) { | |
| console.error('Failed to load draft', e); | |
| } | |
| } | |
| }); | |
| watch(checklist, (newVal) => { | |
| localStorage.setItem('checklist_pro_draft', JSON.stringify(newVal)); | |
| }, { deep: true }); | |
| const isExporting = ref(false); | |
| const themeColors = [ | |
| { value: 'indigo', bgClass: 'bg-indigo-600' }, | |
| { value: 'blue', bgClass: 'bg-blue-600' }, | |
| { value: 'emerald', bgClass: 'bg-emerald-600' }, | |
| { value: 'rose', bgClass: 'bg-rose-600' }, | |
| { value: 'amber', bgClass: 'bg-amber-600' }, | |
| { value: 'slate', bgClass: 'bg-slate-800' }, | |
| ]; | |
| const currentThemeClass = computed(() => { | |
| const map = { | |
| 'indigo': 'bg-indigo-600', | |
| 'blue': 'bg-blue-600', | |
| 'emerald': 'bg-emerald-600', | |
| 'rose': 'bg-rose-600', | |
| 'amber': 'bg-amber-600', | |
| 'slate': 'bg-slate-800', | |
| }; | |
| return map[checklist.value.theme] || 'bg-indigo-600'; | |
| }); | |
| const currentThemeTextClass = computed(() => { | |
| const map = { | |
| 'indigo': 'text-indigo-600', | |
| 'blue': 'text-blue-600', | |
| 'emerald': 'text-emerald-600', | |
| 'rose': 'text-rose-600', | |
| 'amber': 'text-amber-600', | |
| 'slate': 'text-slate-800', | |
| }; | |
| return map[checklist.value.theme] || 'text-indigo-600'; | |
| }); | |
| const totalItems = computed(() => { | |
| return checklist.value.groups.reduce((acc, g) => acc + g.items.length, 0); | |
| }); | |
| // Actions | |
| const addGroup = () => { | |
| checklist.value.groups.push({ title: '', items: [] }); | |
| }; | |
| const removeGroup = (index) => { | |
| if(confirm('确定删除此分组吗?')) checklist.value.groups.splice(index, 1); | |
| }; | |
| const moveGroup = (index, direction) => { | |
| const newIndex = index + direction; | |
| if (newIndex >= 0 && newIndex < checklist.value.groups.length) { | |
| const temp = checklist.value.groups[index]; | |
| checklist.value.groups[index] = checklist.value.groups[newIndex]; | |
| checklist.value.groups[newIndex] = temp; | |
| } | |
| }; | |
| const addItem = (gIndex) => { | |
| checklist.value.groups[gIndex].items.push({ text: '', note: '', link: '' }); | |
| }; | |
| const removeItem = (gIndex, iIndex) => { | |
| checklist.value.groups[gIndex].items.splice(iIndex, 1); | |
| }; | |
| const moveItem = (gIndex, iIndex, direction) => { | |
| const items = checklist.value.groups[gIndex].items; | |
| const newIndex = iIndex + direction; | |
| if (newIndex >= 0 && newIndex < items.length) { | |
| const temp = items[iIndex]; | |
| items[iIndex] = items[newIndex]; | |
| items[newIndex] = temp; | |
| } | |
| }; | |
| const downloadHTML = async () => { | |
| isExporting.value = true; | |
| try { | |
| const response = await fetch('/download', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(checklist.value) | |
| }); | |
| if (!response.ok) throw new Error('Export failed'); | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `checklist-${Date.now()}.html`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| window.URL.revokeObjectURL(url); | |
| } catch (e) { | |
| alert('导出失败: ' + e.message); | |
| } finally { | |
| isExporting.value = false; | |
| } | |
| }; | |
| return { | |
| checklist, | |
| themeColors, | |
| currentThemeClass, | |
| currentThemeTextClass, | |
| totalItems, | |
| addGroup, removeGroup, moveGroup, | |
| addItem, removeItem, moveItem, | |
| downloadHTML, isExporting | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.readAsText(file, 'UTF-8'); | |
| reader.onload = readerEvent => { | |
| try { | |
| const content = readerEvent.target.result; | |
| checklist.value = JSON.parse(content); | |
| } catch (e) { | |
| alert('Invalid JSON file'); | |
| } | |
| } | |
| } | |
| input.click(); | |
| }; | |
| const clearStorage = () => { | |
| if(confirm('确定要清空本地缓存并重置为默认值吗?此操作无法撤销。')) { | |
| localStorage.removeItem('checklist_pro_draft'); | |
| location.reload(); | |
| } | |
| }; | |
| return { | |
| checklist, | |
| themeColors, | |
| currentThemeClass, | |
| currentThemeTextClass, | |
| totalItems, | |
| addGroup, removeGroup, moveGroup, | |
| addItem, removeItem, moveItem, | |
| downloadHTML, isExporting, | |
| exportJSON, importJSON, clearStorage | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |