Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Idea Validator Pro - 创意验证专家</title> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { font-family: 'Inter', sans-serif; background-color: #f8fafc; } | |
| .slide-enter-active, .slide-leave-active { transition: all 0.3s ease; } | |
| .slide-enter-from, .slide-leave-to { opacity: 0; transform: translateY(10px); } | |
| .canvas-grid { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1px; | |
| background-color: #e2e8f0; | |
| border: 1px solid #e2e8f0; | |
| } | |
| .canvas-cell { background: white; padding: 1rem; display: flex; flex-direction: column; min-height: 200px; } | |
| .swot-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 1.5rem; | |
| } | |
| @media (min-width: 1024px) { | |
| .canvas-grid { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| grid-template-rows: repeat(3, minmax(200px, auto)); | |
| } | |
| .cell-problem { grid-area: 1 / 1 / 3 / 2; } | |
| .cell-solution { grid-area: 1 / 2 / 2 / 3; } | |
| .cell-metrics { grid-area: 2 / 2 / 3 / 3; } | |
| .cell-uvp { grid-area: 1 / 3 / 3 / 4; } | |
| .cell-advantage { grid-area: 1 / 4 / 2 / 5; } | |
| .cell-channels { grid-area: 2 / 4 / 3 / 5; } | |
| .cell-segments { grid-area: 1 / 5 / 3 / 6; } | |
| .cell-cost { grid-area: 3 / 1 / 4 / 3; } | |
| .cell-revenue { grid-area: 3 / 3 / 4 / 6; } | |
| } | |
| @media (max-width: 768px) { | |
| .swot-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| {% raw %} | |
| <div id="app" class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white shadow-lg"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"> | |
| <div class="flex items-center gap-2 cursor-pointer" @click="goHome"> | |
| <i class="fa-solid fa-lightbulb text-yellow-300 text-xl"></i> | |
| <h1 class="text-xl font-bold tracking-tight">Idea Validator Pro</h1> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div class="relative group" v-if="currentProject"> | |
| <button class="text-sm bg-white/20 hover:bg-white/30 px-3 py-1.5 rounded transition flex items-center gap-2"> | |
| <i class="fa-solid fa-download"></i> 导出 <i class="fa-solid fa-caret-down text-xs"></i> | |
| </button> | |
| <div class="absolute right-0 top-full mt-2 w-48 bg-white rounded-lg shadow-xl border border-gray-100 hidden group-hover:block z-50 overflow-hidden"> | |
| <a @click="exportProject" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer"> | |
| <i class="fa-solid fa-file-code mr-2 text-gray-400"></i> JSON 数据 | |
| </a> | |
| <a @click="exportMarkdown" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer"> | |
| <i class="fa-brands fa-markdown mr-2 text-gray-400"></i> Markdown 文档 | |
| </a> | |
| </div> | |
| </div> | |
| <a href="https://github.com/duqing026" target="_blank" class="text-white/80 hover:text-white"> | |
| <i class="fa-brands fa-github text-xl"></i> | |
| </a> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-1 bg-gray-50 p-4 sm:p-8 overflow-y-auto"> | |
| <!-- Dashboard (Project List) --> | |
| <div v-if="!currentProject" class="max-w-5xl mx-auto"> | |
| <div class="flex justify-between items-center mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800">我的创意库</h2> | |
| <button @click="createNewProject" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow transition flex items-center gap-2"> | |
| <i class="fa-solid fa-plus"></i> 新建创意 | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <div v-for="p in projects" :key="p.id" | |
| class="bg-white rounded-xl shadow-sm hover:shadow-md transition cursor-pointer border border-gray-100 overflow-hidden group" | |
| @click="openProject(p.id)"> | |
| <div class="h-2 bg-gradient-to-r from-blue-500 to-indigo-500"></div> | |
| <div class="p-6"> | |
| <h3 class="text-lg font-bold text-gray-800 mb-2 truncate">{{ p.title || '未命名创意' }}</h3> | |
| <p class="text-gray-500 text-sm mb-4 line-clamp-2 h-10">{{ p.pitch || '暂无简介...' }}</p> | |
| <div class="flex justify-between items-center text-xs text-gray-400"> | |
| <span><i class="fa-regular fa-clock mr-1"></i>{{ formatDate(p.updatedAt) }}</span> | |
| <div class="flex gap-2"> | |
| <span class="px-2 py-1 bg-gray-100 rounded text-gray-600">{{ p.experiments ? p.experiments.length : 0 }} 实验</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="px-6 py-3 bg-gray-50 border-t border-gray-100 flex justify-end opacity-0 group-hover:opacity-100 transition"> | |
| <button @click.stop="deleteProject(p.id)" class="text-red-500 hover:text-red-700 text-sm"> | |
| <i class="fa-solid fa-trash"></i> 删除 | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Empty State --> | |
| <div v-if="projects.length === 0" class="col-span-full text-center py-20 bg-white rounded-xl border border-dashed border-gray-300"> | |
| <div class="text-6xl text-gray-200 mb-4"><i class="fa-solid fa-rocket"></i></div> | |
| <p class="text-gray-500 mb-4">还没有任何创意项目</p> | |
| <button @click="createNewProject" class="text-blue-600 hover:underline">立即开始第一个创意验证</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Project Editor --> | |
| <div v-else class="max-w-7xl mx-auto h-full flex flex-col"> | |
| <!-- Navigation Tabs --> | |
| <div class="flex flex-wrap gap-2 mb-6 border-b border-gray-200 pb-1"> | |
| <button v-for="tab in tabs" :key="tab.id" | |
| @click="currentTab = tab.id" | |
| class="px-4 py-2 rounded-t-lg font-medium text-sm transition-colors relative top-px" | |
| :class="currentTab === tab.id ? 'bg-white text-blue-600 border border-gray-200 border-b-white' : 'text-gray-500 hover:text-gray-700'"> | |
| <i :class="tab.icon + ' mr-2'"></i>{{ tab.name }} | |
| </button> | |
| </div> | |
| <!-- Tab Content --> | |
| <div class="bg-white rounded-b-lg rounded-tr-lg shadow-sm border border-gray-200 min-h-[500px] flex-1"> | |
| <!-- 1. Concept Tab --> | |
| <div v-if="currentTab === 'concept'" class="p-8 max-w-3xl mx-auto animate-fade-in"> | |
| <div class="mb-8"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">项目名称</label> | |
| <input v-model="currentProject.title" @input="save" type="text" class="w-full text-3xl font-bold border-b-2 border-gray-200 focus:border-blue-500 outline-none py-2 bg-transparent placeholder-gray-300" placeholder="给你的创意起个名字"> | |
| </div> | |
| <div class="mb-8"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">一句话简介 (Elevator Pitch)</label> | |
| <textarea v-model="currentProject.pitch" @input="save" rows="3" class="w-full p-4 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-100 focus:border-blue-500 outline-none resize-none" placeholder="用一句话描述你的产品解决了谁的什么问题..."></textarea> | |
| <p class="text-xs text-gray-400 mt-2 text-right">💡 提示:我们帮助 [目标用户] 解决 [痛点],通过 [解决方案]。</p> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">目标用户 (Who)</label> | |
| <textarea v-model="currentProject.who" @input="save" rows="4" class="w-full p-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-100 focus:border-blue-500 outline-none" placeholder="谁会最先使用这个产品?"></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">核心痛点 (Why)</label> | |
| <textarea v-model="currentProject.why" @input="save" rows="4" class="w-full p-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-100 focus:border-blue-500 outline-none" placeholder="他们现在面临什么困扰?"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 2. Lean Canvas Tab --> | |
| <div v-if="currentTab === 'canvas'" class="p-4 h-full flex flex-col"> | |
| <div class="flex justify-between items-center mb-4 px-2"> | |
| <h3 class="font-bold text-gray-700">精益画布 (Lean Canvas)</h3> | |
| <button @click="downloadCanvas" class="text-xs bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded text-gray-600"> | |
| <i class="fa-solid fa-image mr-1"></i> 下载图片 | |
| </button> | |
| </div> | |
| <div id="lean-canvas" class="canvas-grid rounded-lg overflow-hidden shadow-sm flex-1 text-sm"> | |
| <canvas-cell title="问题 (Problem)" icon="fa-triangle-exclamation" v-model="currentProject.canvas.problem" area="cell-problem"></canvas-cell> | |
| <canvas-cell title="解决方案 (Solution)" icon="fa-puzzle-piece" v-model="currentProject.canvas.solution" area="cell-solution"></canvas-cell> | |
| <canvas-cell title="独特卖点 (UVP)" icon="fa-gift" v-model="currentProject.canvas.uvp" area="cell-uvp"></canvas-cell> | |
| <canvas-cell title="竞争壁垒 (Advantage)" icon="fa-shield-halved" v-model="currentProject.canvas.advantage" area="cell-advantage"></canvas-cell> | |
| <canvas-cell title="客户细分 (Segments)" icon="fa-users" v-model="currentProject.canvas.segments" area="cell-segments"></canvas-cell> | |
| <canvas-cell title="关键指标 (Metrics)" icon="fa-chart-line" v-model="currentProject.canvas.metrics" area="cell-metrics"></canvas-cell> | |
| <canvas-cell title="渠道 (Channels)" icon="fa-bullhorn" v-model="currentProject.canvas.channels" area="cell-channels"></canvas-cell> | |
| <canvas-cell title="成本结构 (Cost)" icon="fa-file-invoice-dollar" v-model="currentProject.canvas.cost" area="cell-cost"></canvas-cell> | |
| <canvas-cell title="收入来源 (Revenue)" icon="fa-money-bill-wave" v-model="currentProject.canvas.revenue" area="cell-revenue"></canvas-cell> | |
| </div> | |
| </div> | |
| <!-- 3. SWOT Tab (New) --> | |
| <div v-if="currentTab === 'swot'" class="p-6 h-full overflow-y-auto"> | |
| <div class="mb-6"> | |
| <h3 class="font-bold text-gray-700">SWOT 分析</h3> | |
| <p class="text-sm text-gray-500">评估项目的优势、劣势、机会和威胁。</p> | |
| </div> | |
| <div class="swot-grid"> | |
| <!-- Strengths --> | |
| <div class="bg-green-50 rounded-lg p-4 border border-green-100"> | |
| <div class="flex items-center gap-2 mb-3 text-green-700 font-bold uppercase tracking-wide"> | |
| <i class="fa-solid fa-dumbbell"></i> 优势 (Strengths) | |
| </div> | |
| <canvas-cell title="" icon="" v-model="currentProject.swot.strengths" area=""></canvas-cell> | |
| </div> | |
| <!-- Weaknesses --> | |
| <div class="bg-red-50 rounded-lg p-4 border border-red-100"> | |
| <div class="flex items-center gap-2 mb-3 text-red-700 font-bold uppercase tracking-wide"> | |
| <i class="fa-solid fa-link-slash"></i> 劣势 (Weaknesses) | |
| </div> | |
| <canvas-cell title="" icon="" v-model="currentProject.swot.weaknesses" area=""></canvas-cell> | |
| </div> | |
| <!-- Opportunities --> | |
| <div class="bg-blue-50 rounded-lg p-4 border border-blue-100"> | |
| <div class="flex items-center gap-2 mb-3 text-blue-700 font-bold uppercase tracking-wide"> | |
| <i class="fa-solid fa-lightbulb"></i> 机会 (Opportunities) | |
| </div> | |
| <canvas-cell title="" icon="" v-model="currentProject.swot.opportunities" area=""></canvas-cell> | |
| </div> | |
| <!-- Threats --> | |
| <div class="bg-yellow-50 rounded-lg p-4 border border-yellow-100"> | |
| <div class="flex items-center gap-2 mb-3 text-yellow-700 font-bold uppercase tracking-wide"> | |
| <i class="fa-solid fa-bomb"></i> 威胁 (Threats) | |
| </div> | |
| <canvas-cell title="" icon="" v-model="currentProject.swot.threats" area=""></canvas-cell> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 4. Experiments Tab --> | |
| <div v-if="currentTab === 'experiments'" class="p-6 bg-gray-50 h-full overflow-y-auto"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <div> | |
| <h3 class="text-lg font-bold text-gray-800">验证实验</h3> | |
| <p class="text-sm text-gray-500">不要只猜想,去验证!</p> | |
| </div> | |
| <button @click="addExperiment" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded shadow text-sm"> | |
| <i class="fa-solid fa-flask mr-1"></i> 新建实验 | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div v-for="(exp, idx) in currentProject.experiments" :key="idx" class="bg-white p-5 rounded-lg border border-gray-200 shadow-sm relative group"> | |
| <div class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition"> | |
| <button @click="removeExperiment(idx)" class="text-gray-400 hover:text-red-500"><i class="fa-solid fa-trash"></i></button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="col-span-1"> | |
| <label class="block text-xs font-bold text-gray-500 uppercase mb-1">假设 (Hypothesis)</label> | |
| <textarea v-model="exp.hypothesis" @change="save" rows="3" class="w-full text-sm p-2 border border-gray-200 rounded bg-yellow-50 focus:border-indigo-500 outline-none" placeholder="如果..."></textarea> | |
| </div> | |
| <div class="col-span-1"> | |
| <label class="block text-xs font-bold text-gray-500 uppercase mb-1">实验方法 (Method)</label> | |
| <textarea v-model="exp.method" @change="save" rows="3" class="w-full text-sm p-2 border border-gray-200 rounded focus:border-indigo-500 outline-none" placeholder="我们将..."></textarea> | |
| </div> | |
| <div class="col-span-1"> | |
| <label class="block text-xs font-bold text-gray-500 uppercase mb-1">结果/结论 (Result)</label> | |
| <textarea v-model="exp.result" @change="save" rows="3" class="w-full text-sm p-2 border border-gray-200 rounded focus:border-indigo-500 outline-none" :class="exp.status === 'success' ? 'bg-green-50' : (exp.status === 'fail' ? 'bg-red-50' : '')" placeholder="结果是..."></textarea> | |
| </div> | |
| </div> | |
| <div class="mt-3 flex items-center gap-3 pt-3 border-t border-gray-100"> | |
| <span class="text-xs font-medium text-gray-500">状态:</span> | |
| <select v-model="exp.status" @change="save" class="text-xs border border-gray-300 rounded px-2 py-1 outline-none"> | |
| <option value="planned">📅 计划中</option> | |
| <option value="running">🏃 进行中</option> | |
| <option value="success">✅ 验证成功</option> | |
| <option value="fail">❌ 验证失败</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div v-if="currentProject.experiments.length === 0" class="text-center py-10 text-gray-400 border-2 border-dashed border-gray-200 rounded-lg"> | |
| 还没有实验。试着添加一个:"如果在朋友圈发海报,会有10个人扫码。" | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Canvas Cell Component Template --> | |
| <template id="canvas-cell-template"> | |
| <div :class="['canvas-cell', area, 'group']"> | |
| <div class="flex items-center gap-2 mb-2 text-gray-600"> | |
| <i :class="['fa-solid', icon, 'text-gray-400']"></i> | |
| <h4 class="font-bold text-xs uppercase tracking-wider">{{ title }}</h4> | |
| </div> | |
| <div class="flex-1 overflow-y-auto"> | |
| <ul class="space-y-1"> | |
| <li v-for="(item, idx) in modelValue" :key="idx" class="flex gap-2 group/item"> | |
| <span class="text-gray-400 text-xs">•</span> | |
| <input v-model="modelValue[idx]" @change="$emit('update:modelValue', modelValue)" | |
| class="w-full text-sm bg-transparent outline-none border-b border-transparent hover:border-gray-200 focus:border-blue-400 pb-0.5" | |
| @keyup.enter="focusNext($event)" | |
| placeholder="..."> | |
| <button @click="removeItem(idx)" class="text-gray-300 hover:text-red-400 opacity-0 group-hover/item:opacity-100 px-1 text-xs">×</button> | |
| </li> | |
| </ul> | |
| <button @click="addItem" class="mt-2 text-xs text-blue-500 hover:text-blue-700 opacity-0 group-hover:opacity-100 transition flex items-center gap-1"> | |
| <i class="fa-solid fa-plus"></i> 添加 | |
| </button> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| const { createApp, ref, computed, onMounted, watch } = Vue; | |
| const CanvasCell = { | |
| template: '#canvas-cell-template', | |
| props: ['title', 'icon', 'area', 'modelValue'], | |
| emits: ['update:modelValue'], | |
| setup(props, { emit }) { | |
| const addItem = () => { | |
| const newList = [...props.modelValue, '']; | |
| emit('update:modelValue', newList); | |
| }; | |
| const removeItem = (idx) => { | |
| const newList = [...props.modelValue]; | |
| newList.splice(idx, 1); | |
| emit('update:modelValue', newList); | |
| }; | |
| return { addItem, removeItem }; | |
| } | |
| }; | |
| createApp({ | |
| components: { CanvasCell }, | |
| setup() { | |
| const DEFAULT_PROJECT = { | |
| id: 'demo-1', | |
| title: '智能周报生成器 (示例)', | |
| pitch: '帮助程序员从繁琐的周报中解脱出来,自动从代码提交记录生成高质量周报。', | |
| who: '互联网公司的程序员、技术经理', | |
| why: '每周五写周报很痛苦,容易遗漏工作内容,且浪费开发时间。', | |
| updatedAt: new Date(), | |
| canvas: { | |
| problem: ['写周报浪费时间', '容易遗漏工作内容', '格式不统一,主管难以阅读'], | |
| solution: ['自动读取 Git Commit', 'AI 润色生成自然语言', '支持导出为 Markdown/PDF'], | |
| metrics: ['周报生成时间从30分钟降至2分钟', '用户留存率'], | |
| uvp: ['一键生成', '智能润色', '多平台集成 (Jira/GitHub)'], | |
| advantage: ['先发优势', '高质量的 AI Prompt 库'], | |
| channels: ['技术博客', 'GitHub Trending', '技术社区 (掘金/V2EX)'], | |
| segments: ['中大型互联网公司开发团队', '个人开发者'], | |
| cost: ['OpenAI API 费用', '服务器运维成本'], | |
| revenue: ['个人版免费', '团队版 $5/人/月'] | |
| }, | |
| swot: { | |
| strengths: ['自动化程度高', '支持多种导出格式'], | |
| weaknesses: ['依赖 OpenAI API,成本较高', '初次配置较繁琐'], | |
| opportunities: ['远程办公趋势增加', '开发者工具市场增长'], | |
| threats: ['GitHub Copilot 等竞品集成类似功能'] | |
| }, | |
| experiments: [ | |
| { | |
| hypothesis: '如果我在 V2EX 发布产品介绍,会有至少 50 人注册试用。', | |
| method: '撰写一篇详细的软文,介绍产品痛点和解决方案,发布到 V2EX 创造节点。', | |
| result: '获得了 120 个注册用户,但只有 10% 留存。', | |
| status: 'success' | |
| } | |
| ] | |
| }; | |
| const projects = ref([]); | |
| const currentProject = ref(null); | |
| const currentTab = ref('concept'); | |
| const tabs = [ | |
| { id: 'concept', name: '概念', icon: 'fa-solid fa-lightbulb' }, | |
| { id: 'canvas', name: '画布', icon: 'fa-solid fa-table-cells' }, | |
| { id: 'swot', name: 'SWOT 分析', icon: 'fa-solid fa-shield-halved' }, | |
| { id: 'experiments', name: '实验', icon: 'fa-solid fa-flask' } | |
| ]; | |
| const createNewProject = () => { | |
| const newProject = { | |
| id: Date.now().toString(), | |
| title: '未命名创意', | |
| pitch: '', | |
| who: '', | |
| why: '', | |
| updatedAt: new Date(), | |
| canvas: { | |
| problem: [], solution: [], metrics: [], uvp: [], | |
| advantage: [], channels: [], segments: [], cost: [], revenue: [] | |
| }, | |
| swot: { | |
| strengths: [], weaknesses: [], opportunities: [], threats: [] | |
| }, | |
| experiments: [] | |
| }; | |
| projects.value.unshift(newProject); | |
| currentProject.value = newProject; | |
| save(); | |
| }; | |
| const openProject = (id) => { | |
| currentProject.value = projects.value.find(p => p.id === id); | |
| currentTab.value = 'concept'; | |
| }; | |
| const goHome = () => { | |
| currentProject.value = null; | |
| }; | |
| const deleteProject = (id) => { | |
| if (confirm('确定删除这个项目吗?')) { | |
| projects.value = projects.value.filter(p => p.id !== id); | |
| if (currentProject.value && currentProject.value.id === id) { | |
| currentProject.value = null; | |
| } | |
| save(); | |
| } | |
| }; | |
| const addExperiment = () => { | |
| currentProject.value.experiments.push({ | |
| hypothesis: '', method: '', result: '', status: 'planned' | |
| }); | |
| save(); | |
| }; | |
| const removeExperiment = (idx) => { | |
| currentProject.value.experiments.splice(idx, 1); | |
| save(); | |
| }; | |
| const formatDate = (dateStr) => { | |
| return new Date(dateStr).toLocaleDateString(); | |
| }; | |
| const save = () => { | |
| if (currentProject.value) { | |
| currentProject.value.updatedAt = new Date(); | |
| } | |
| localStorage.setItem('idea-validator-pro-data', JSON.stringify(projects.value)); | |
| }; | |
| const load = () => { | |
| const data = localStorage.getItem('idea-validator-pro-data'); | |
| if (data) { | |
| try { | |
| projects.value = JSON.parse(data); | |
| } catch(e) { | |
| console.error("Data parse error", e); | |
| projects.value = []; | |
| } | |
| } | |
| // 数据迁移:确保所有项目都有 swot 字段 | |
| projects.value.forEach(p => { | |
| if (!p.swot) { | |
| p.swot = { strengths: [], weaknesses: [], opportunities: [], threats: [] }; | |
| } | |
| }); | |
| if (projects.value.length === 0) { | |
| // 使用 JSON.parse(JSON.stringify(...)) 深拷贝,避免引用问题 | |
| projects.value = [JSON.parse(JSON.stringify(DEFAULT_PROJECT))]; | |
| save(); | |
| } | |
| }; | |
| const downloadCanvas = () => { | |
| const element = document.getElementById('lean-canvas'); | |
| html2canvas(element).then(canvas => { | |
| const link = document.createElement('a'); | |
| link.download = `${currentProject.value.title}-canvas.png`; | |
| link.href = canvas.toDataURL(); | |
| link.click(); | |
| }); | |
| }; | |
| const exportProject = () => { | |
| const dataStr = JSON.stringify(currentProject.value, null, 2); | |
| const blob = new Blob([dataStr], {type: "application/json"}); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.download = `project-${currentProject.value.title}.json`; | |
| link.href = url; | |
| link.click(); | |
| }; | |
| const exportMarkdown = () => { | |
| const p = currentProject.value; | |
| let md = `# ${p.title}\n\n`; | |
| md += `> ${p.pitch}\n\n`; | |
| md += `**目标用户**: ${p.who}\n\n`; | |
| md += `**核心痛点**: ${p.why}\n\n`; | |
| md += `## 精益画布\n\n`; | |
| md += `### 1. 问题 (Problem)\n${p.canvas.problem.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 2. 解决方案 (Solution)\n${p.canvas.solution.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 3. 独特卖点 (UVP)\n${p.canvas.uvp.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 4. 竞争壁垒 (Advantage)\n${p.canvas.advantage.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 5. 客户细分 (Segments)\n${p.canvas.segments.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 6. 关键指标 (Metrics)\n${p.canvas.metrics.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 7. 渠道 (Channels)\n${p.canvas.channels.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 8. 成本结构 (Cost)\n${p.canvas.cost.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 9. 收入来源 (Revenue)\n${p.canvas.revenue.map(i => `- ${i}`).join('\n')}\n\n`; | |
| if (p.swot) { | |
| md += `## SWOT 分析\n\n`; | |
| md += `### 优势 (Strengths)\n${p.swot.strengths.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 劣势 (Weaknesses)\n${p.swot.weaknesses.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 机会 (Opportunities)\n${p.swot.opportunities.map(i => `- ${i}`).join('\n')}\n\n`; | |
| md += `### 威胁 (Threats)\n${p.swot.threats.map(i => `- ${i}`).join('\n')}\n\n`; | |
| } | |
| md += `## 验证实验\n\n`; | |
| p.experiments.forEach((exp, idx) => { | |
| md += `### 实验 ${idx + 1}\n`; | |
| md += `- **假设**: ${exp.hypothesis}\n`; | |
| md += `- **方法**: ${exp.method}\n`; | |
| md += `- **结果**: ${exp.result}\n`; | |
| md += `- **状态**: ${exp.status}\n\n`; | |
| }); | |
| const blob = new Blob([md], {type: "text/markdown"}); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.download = `${p.title}-report.md`; | |
| link.href = url; | |
| link.click(); | |
| }; | |
| onMounted(() => { | |
| load(); | |
| }); | |
| return { | |
| projects, currentProject, currentTab, tabs, | |
| createNewProject, openProject, goHome, deleteProject, | |
| addExperiment, removeExperiment, | |
| save, formatDate, downloadCanvas, exportProject, exportMarkdown | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| {% endraw %} | |
| </body> | |
| </html> | |