duqing2026 commited on
Commit
53cf867
·
0 Parent(s):

Initial commit of Idea Validator Pro

Browse files
Files changed (5) hide show
  1. Dockerfile +16 -0
  2. README.md +45 -0
  3. app.py +16 -0
  4. requirements.txt +2 -0
  5. templates/index.html +374 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install Flask and Gunicorn
6
+ RUN pip install --no-cache-dir flask gunicorn
7
+
8
+ COPY . .
9
+
10
+ # Create a non-root user for Hugging Face Spaces
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+ ENV HOME=/home/user \
14
+ PATH=/home/user/.local/bin:$PATH
15
+
16
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Idea Validator Pro
3
+ emoji: 💡
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ short_description: A comprehensive tool for validating startup ideas with Lean Canvas and experiments.
9
+ ---
10
+
11
+ # Idea Validator Pro (创意验证专家)
12
+
13
+ 一个帮助创业者和独立开发者验证产品创意的全流程工具。
14
+ 从概念到画布,再到实验验证,Idea Validator Pro 帮助你结构化思考,减少试错成本。
15
+
16
+ ## 功能特色 (Features)
17
+
18
+ 1. **创意管理**: 记录和管理你的多个产品创意。
19
+ 2. **概念打磨 (Concept)**:
20
+ - 一句话简介 (Elevator Pitch)
21
+ - 目标用户 (Who) & 核心痛点 (Why)
22
+ 3. **精益画布 (Lean Canvas)**:
23
+ - 内置标准精益画布模板 (9格模型)。
24
+ - 交互式编辑,支持一键下载为图片。
25
+ 4. **实验验证 (Experiments)**:
26
+ - 记录你的假设 (Hypothesis)。
27
+ - 设计验证方法 (Method)。
28
+ - 追踪实验结果 (Result)。
29
+ - 标记状态 (计划中/进行中/成功/失败)。
30
+ 5. **本地优先 (Local First)**:
31
+ - 所有数据存储在浏览器 LocalStorage。
32
+ - 支持 JSON 导出/备份。
33
+
34
+ ## 技术栈 (Tech Stack)
35
+
36
+ - **Frontend**: Vue 3 (CDN), Tailwind CSS (CDN), html2canvas
37
+ - **Backend**: Flask (Python) - Static serving
38
+ - **Deployment**: Docker
39
+
40
+ ## 快速开始
41
+
42
+ ```bash
43
+ python app.py
44
+ ```
45
+ 访问 http://localhost:7860
app.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, send_from_directory
2
+ import os
3
+
4
+ app = Flask(__name__, static_folder='static')
5
+
6
+ @app.route('/')
7
+ def index():
8
+ return render_template('index.html')
9
+
10
+ @app.route('/static/<path:path>')
11
+ def send_static(path):
12
+ return send_from_directory('static', path)
13
+
14
+ if __name__ == '__main__':
15
+ port = int(os.environ.get('PORT', 7860))
16
+ app.run(debug=True, host='0.0.0.0', port=port)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask
2
+ gunicorn
templates/index.html ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Idea Validator Pro - 创意验证专家</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
10
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
13
+ body { font-family: 'Inter', sans-serif; background-color: #f8fafc; }
14
+ .slide-enter-active, .slide-leave-active { transition: all 0.3s ease; }
15
+ .slide-enter-from, .slide-leave-to { opacity: 0; transform: translateY(10px); }
16
+ .canvas-grid {
17
+ display: grid;
18
+ grid-template-columns: repeat(5, 1fr);
19
+ grid-template-rows: repeat(3, minmax(200px, auto));
20
+ gap: 1px;
21
+ background-color: #e2e8f0;
22
+ border: 1px solid #e2e8f0;
23
+ }
24
+ .canvas-cell { background: white; padding: 1rem; display: flex; flex-direction: column; }
25
+ .cell-problem { grid-area: 1 / 1 / 3 / 2; }
26
+ .cell-solution { grid-area: 1 / 2 / 2 / 3; }
27
+ .cell-metrics { grid-area: 2 / 2 / 3 / 3; }
28
+ .cell-uvp { grid-area: 1 / 3 / 3 / 4; }
29
+ .cell-advantage { grid-area: 1 / 4 / 2 / 5; }
30
+ .cell-channels { grid-area: 2 / 4 / 3 / 5; }
31
+ .cell-segments { grid-area: 1 / 5 / 3 / 6; }
32
+ .cell-cost { grid-area: 3 / 1 / 4 / 3; }
33
+ .cell-revenue { grid-area: 3 / 3 / 4 / 6; }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div id="app" class="min-h-screen flex flex-col">
38
+ <!-- Header -->
39
+ <header class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white shadow-lg">
40
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
41
+ <div class="flex items-center gap-2 cursor-pointer" @click="goHome">
42
+ <i class="fa-solid fa-lightbulb text-yellow-300 text-xl"></i>
43
+ <h1 class="text-xl font-bold tracking-tight">Idea Validator Pro</h1>
44
+ </div>
45
+ <div class="flex items-center gap-4">
46
+ <button v-if="currentProject" @click="exportProject" class="text-sm bg-white/20 hover:bg-white/30 px-3 py-1.5 rounded transition">
47
+ <i class="fa-solid fa-download mr-1"></i> 导出
48
+ </button>
49
+ <a href="https://github.com/duqing026" target="_blank" class="text-white/80 hover:text-white">
50
+ <i class="fa-brands fa-github text-xl"></i>
51
+ </a>
52
+ </div>
53
+ </div>
54
+ </header>
55
+
56
+ <!-- Main Content -->
57
+ <main class="flex-1 bg-gray-50 p-4 sm:p-8 overflow-y-auto">
58
+
59
+ <!-- Dashboard (Project List) -->
60
+ <div v-if="!currentProject" class="max-w-5xl mx-auto">
61
+ <div class="flex justify-between items-center mb-8">
62
+ <h2 class="text-2xl font-bold text-gray-800">我的创意库</h2>
63
+ <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">
64
+ <i class="fa-solid fa-plus"></i> 新建创意
65
+ </button>
66
+ </div>
67
+
68
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
69
+ <div v-for="p in projects" :key="p.id"
70
+ class="bg-white rounded-xl shadow-sm hover:shadow-md transition cursor-pointer border border-gray-100 overflow-hidden group"
71
+ @click="openProject(p.id)">
72
+ <div class="h-2 bg-gradient-to-r from-blue-500 to-indigo-500"></div>
73
+ <div class="p-6">
74
+ <h3 class="text-lg font-bold text-gray-800 mb-2 truncate">{{ p.title || '未命名创意' }}</h3>
75
+ <p class="text-gray-500 text-sm mb-4 line-clamp-2 h-10">{{ p.pitch || '暂无简介...' }}</p>
76
+ <div class="flex justify-between items-center text-xs text-gray-400">
77
+ <span><i class="fa-regular fa-clock mr-1"></i>{{ formatDate(p.updatedAt) }}</span>
78
+ <div class="flex gap-2">
79
+ <span class="px-2 py-1 bg-gray-100 rounded text-gray-600">{{ p.experiments ? p.experiments.length : 0 }} 实验</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ <div class="px-6 py-3 bg-gray-50 border-t border-gray-100 flex justify-end opacity-0 group-hover:opacity-100 transition">
84
+ <button @click.stop="deleteProject(p.id)" class="text-red-500 hover:text-red-700 text-sm">
85
+ <i class="fa-solid fa-trash"></i> 删除
86
+ </button>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Empty State -->
91
+ <div v-if="projects.length === 0" class="col-span-full text-center py-20 bg-white rounded-xl border border-dashed border-gray-300">
92
+ <div class="text-6xl text-gray-200 mb-4"><i class="fa-solid fa-rocket"></i></div>
93
+ <p class="text-gray-500 mb-4">还没有任何创意项目</p>
94
+ <button @click="createNewProject" class="text-blue-600 hover:underline">立即开始第一个创意验证</button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Project Editor -->
100
+ <div v-else class="max-w-7xl mx-auto h-full flex flex-col">
101
+ <!-- Navigation Tabs -->
102
+ <div class="flex flex-wrap gap-2 mb-6 border-b border-gray-200 pb-1">
103
+ <button v-for="tab in tabs" :key="tab.id"
104
+ @click="currentTab = tab.id"
105
+ class="px-4 py-2 rounded-t-lg font-medium text-sm transition-colors relative top-px"
106
+ :class="currentTab === tab.id ? 'bg-white text-blue-600 border border-gray-200 border-b-white' : 'text-gray-500 hover:text-gray-700'">
107
+ <i :class="tab.icon + ' mr-2'"></i>{{ tab.name }}
108
+ </button>
109
+ </div>
110
+
111
+ <!-- Tab Content -->
112
+ <div class="bg-white rounded-b-lg rounded-tr-lg shadow-sm border border-gray-200 min-h-[500px] flex-1">
113
+
114
+ <!-- 1. Concept Tab -->
115
+ <div v-if="currentTab === 'concept'" class="p-8 max-w-3xl mx-auto animate-fade-in">
116
+ <div class="mb-8">
117
+ <label class="block text-sm font-medium text-gray-700 mb-1">项目名称</label>
118
+ <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="给你的创意起个名字">
119
+ </div>
120
+
121
+ <div class="mb-8">
122
+ <label class="block text-sm font-medium text-gray-700 mb-2">一句话简介 (Elevator Pitch)</label>
123
+ <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>
124
+ <p class="text-xs text-gray-400 mt-2 text-right">💡 提示:我们帮助 [目标用户] 解决 [痛点],通过 [解决方案]。</p>
125
+ </div>
126
+
127
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
128
+ <div>
129
+ <label class="block text-sm font-medium text-gray-700 mb-2">目标用户 (Who)</label>
130
+ <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>
131
+ </div>
132
+ <div>
133
+ <label class="block text-sm font-medium text-gray-700 mb-2">核心痛点 (Why)</label>
134
+ <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>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- 2. Lean Canvas Tab -->
140
+ <div v-if="currentTab === 'canvas'" class="p-4 h-full flex flex-col">
141
+ <div class="flex justify-between items-center mb-4 px-2">
142
+ <h3 class="font-bold text-gray-700">精益画布 (Lean Canvas)</h3>
143
+ <button @click="downloadCanvas" class="text-xs bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded text-gray-600">
144
+ <i class="fa-solid fa-image mr-1"></i> 下载图片
145
+ </button>
146
+ </div>
147
+ <div id="lean-canvas" class="canvas-grid rounded-lg overflow-hidden shadow-sm flex-1 text-sm">
148
+ <canvas-cell title="问题 (Problem)" icon="fa-triangle-exclamation" v-model="currentProject.canvas.problem" area="cell-problem"></canvas-cell>
149
+ <canvas-cell title="解决方案 (Solution)" icon="fa-puzzle-piece" v-model="currentProject.canvas.solution" area="cell-solution"></canvas-cell>
150
+ <canvas-cell title="独特卖点 (UVP)" icon="fa-gift" v-model="currentProject.canvas.uvp" area="cell-uvp"></canvas-cell>
151
+ <canvas-cell title="竞争壁垒 (Advantage)" icon="fa-shield-halved" v-model="currentProject.canvas.advantage" area="cell-advantage"></canvas-cell>
152
+ <canvas-cell title="客户细分 (Segments)" icon="fa-users" v-model="currentProject.canvas.segments" area="cell-segments"></canvas-cell>
153
+ <canvas-cell title="关键指标 (Metrics)" icon="fa-chart-line" v-model="currentProject.canvas.metrics" area="cell-metrics"></canvas-cell>
154
+ <canvas-cell title="渠道 (Channels)" icon="fa-bullhorn" v-model="currentProject.canvas.channels" area="cell-channels"></canvas-cell>
155
+ <canvas-cell title="成本结构 (Cost)" icon="fa-file-invoice-dollar" v-model="currentProject.canvas.cost" area="cell-cost"></canvas-cell>
156
+ <canvas-cell title="收入来源 (Revenue)" icon="fa-money-bill-wave" v-model="currentProject.canvas.revenue" area="cell-revenue"></canvas-cell>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- 3. Experiments Tab -->
161
+ <div v-if="currentTab === 'experiments'" class="p-6 bg-gray-50 h-full overflow-y-auto">
162
+ <div class="flex justify-between items-center mb-6">
163
+ <div>
164
+ <h3 class="text-lg font-bold text-gray-800">验证实验</h3>
165
+ <p class="text-sm text-gray-500">不要只猜想,去验证!</p>
166
+ </div>
167
+ <button @click="addExperiment" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded shadow text-sm">
168
+ <i class="fa-solid fa-flask mr-1"></i> 新建实验
169
+ </button>
170
+ </div>
171
+
172
+ <div class="space-y-4">
173
+ <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">
174
+ <div class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition">
175
+ <button @click="removeExperiment(idx)" class="text-gray-400 hover:text-red-500"><i class="fa-solid fa-trash"></i></button>
176
+ </div>
177
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
178
+ <div class="col-span-1">
179
+ <label class="block text-xs font-bold text-gray-500 uppercase mb-1">假设 (Hypothesis)</label>
180
+ <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>
181
+ </div>
182
+ <div class="col-span-1">
183
+ <label class="block text-xs font-bold text-gray-500 uppercase mb-1">实验方法 (Method)</label>
184
+ <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>
185
+ </div>
186
+ <div class="col-span-1">
187
+ <label class="block text-xs font-bold text-gray-500 uppercase mb-1">结果/结论 (Result)</label>
188
+ <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>
189
+ </div>
190
+ </div>
191
+ <div class="mt-3 flex items-center gap-3 pt-3 border-t border-gray-100">
192
+ <span class="text-xs font-medium text-gray-500">状态:</span>
193
+ <select v-model="exp.status" @change="save" class="text-xs border border-gray-300 rounded px-2 py-1 outline-none">
194
+ <option value="planned">📅 计划中</option>
195
+ <option value="running">🏃 进行中</option>
196
+ <option value="success">✅ 验证成功</option>
197
+ <option value="fail">❌ 验证失败</option>
198
+ </select>
199
+ </div>
200
+ </div>
201
+
202
+ <div v-if="currentProject.experiments.length === 0" class="text-center py-10 text-gray-400 border-2 border-dashed border-gray-200 rounded-lg">
203
+ 还没有实验。试着添加一个:"如果在朋友圈发海报,会有10个人扫码。"
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ </main>
211
+ </div>
212
+
213
+ <!-- Canvas Cell Component Template -->
214
+ <template id="canvas-cell-template">
215
+ <div :class="['canvas-cell', area, 'group']">
216
+ <div class="flex items-center gap-2 mb-2 text-gray-600">
217
+ <i :class="['fa-solid', icon, 'text-gray-400']"></i>
218
+ <h4 class="font-bold text-xs uppercase tracking-wider">{{ title }}</h4>
219
+ </div>
220
+ <div class="flex-1 overflow-y-auto">
221
+ <ul class="space-y-1">
222
+ <li v-for="(item, idx) in modelValue" :key="idx" class="flex gap-2 group/item">
223
+ <span class="text-gray-400 text-xs">•</span>
224
+ <input v-model="modelValue[idx]" @change="$emit('update:modelValue', modelValue)"
225
+ class="w-full text-sm bg-transparent outline-none border-b border-transparent hover:border-gray-200 focus:border-blue-400 pb-0.5"
226
+ @keyup.enter="focusNext($event)"
227
+ placeholder="...">
228
+ <button @click="removeItem(idx)" class="text-gray-300 hover:text-red-400 opacity-0 group-hover/item:opacity-100 px-1 text-xs">×</button>
229
+ </li>
230
+ </ul>
231
+ <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">
232
+ <i class="fa-solid fa-plus"></i> 添加
233
+ </button>
234
+ </div>
235
+ </div>
236
+ </template>
237
+
238
+ <script>
239
+ const { createApp, ref, computed, onMounted, watch } = Vue;
240
+
241
+ const CanvasCell = {
242
+ template: '#canvas-cell-template',
243
+ props: ['title', 'icon', 'area', 'modelValue'],
244
+ emits: ['update:modelValue'],
245
+ setup(props, { emit }) {
246
+ const addItem = () => {
247
+ const newList = [...props.modelValue, ''];
248
+ emit('update:modelValue', newList);
249
+ };
250
+ const removeItem = (idx) => {
251
+ const newList = [...props.modelValue];
252
+ newList.splice(idx, 1);
253
+ emit('update:modelValue', newList);
254
+ };
255
+ return { addItem, removeItem };
256
+ }
257
+ };
258
+
259
+ createApp({
260
+ components: { CanvasCell },
261
+ setup() {
262
+ const projects = ref([]);
263
+ const currentProject = ref(null);
264
+ const currentTab = ref('concept');
265
+
266
+ const tabs = [
267
+ { id: 'concept', name: '概念', icon: 'fa-solid fa-lightbulb' },
268
+ { id: 'canvas', name: '画布', icon: 'fa-solid fa-table-cells' },
269
+ { id: 'experiments', name: '实验', icon: 'fa-solid fa-flask' }
270
+ ];
271
+
272
+ const createNewProject = () => {
273
+ const newProject = {
274
+ id: Date.now().toString(),
275
+ title: '未命名创意',
276
+ pitch: '',
277
+ who: '',
278
+ why: '',
279
+ updatedAt: new Date(),
280
+ canvas: {
281
+ problem: [], solution: [], metrics: [], uvp: [],
282
+ advantage: [], channels: [], segments: [], cost: [], revenue: []
283
+ },
284
+ experiments: []
285
+ };
286
+ projects.value.unshift(newProject);
287
+ currentProject.value = newProject;
288
+ save();
289
+ };
290
+
291
+ const openProject = (id) => {
292
+ currentProject.value = projects.value.find(p => p.id === id);
293
+ currentTab.value = 'concept';
294
+ };
295
+
296
+ const goHome = () => {
297
+ currentProject.value = null;
298
+ };
299
+
300
+ const deleteProject = (id) => {
301
+ if (confirm('确定删除这个项目吗?')) {
302
+ projects.value = projects.value.filter(p => p.id !== id);
303
+ if (currentProject.value && currentProject.value.id === id) {
304
+ currentProject.value = null;
305
+ }
306
+ save();
307
+ }
308
+ };
309
+
310
+ const addExperiment = () => {
311
+ currentProject.value.experiments.push({
312
+ hypothesis: '', method: '', result: '', status: 'planned'
313
+ });
314
+ save();
315
+ };
316
+
317
+ const removeExperiment = (idx) => {
318
+ currentProject.value.experiments.splice(idx, 1);
319
+ save();
320
+ };
321
+
322
+ const formatDate = (dateStr) => {
323
+ return new Date(dateStr).toLocaleDateString();
324
+ };
325
+
326
+ const save = () => {
327
+ if (currentProject.value) {
328
+ currentProject.value.updatedAt = new Date();
329
+ }
330
+ localStorage.setItem('idea-validator-pro-data', JSON.stringify(projects.value));
331
+ };
332
+
333
+ const load = () => {
334
+ const data = localStorage.getItem('idea-validator-pro-data');
335
+ if (data) {
336
+ projects.value = JSON.parse(data);
337
+ }
338
+ };
339
+
340
+ const downloadCanvas = () => {
341
+ const element = document.getElementById('lean-canvas');
342
+ html2canvas(element).then(canvas => {
343
+ const link = document.createElement('a');
344
+ link.download = `${currentProject.value.title}-canvas.png`;
345
+ link.href = canvas.toDataURL();
346
+ link.click();
347
+ });
348
+ };
349
+
350
+ const exportProject = () => {
351
+ const dataStr = JSON.stringify(currentProject.value, null, 2);
352
+ const blob = new Blob([dataStr], {type: "application/json"});
353
+ const url = URL.createObjectURL(blob);
354
+ const link = document.createElement('a');
355
+ link.download = `project-${currentProject.value.title}.json`;
356
+ link.href = url;
357
+ link.click();
358
+ };
359
+
360
+ onMounted(() => {
361
+ load();
362
+ });
363
+
364
+ return {
365
+ projects, currentProject, currentTab, tabs,
366
+ createNewProject, openProject, goHome, deleteProject,
367
+ addExperiment, removeExperiment,
368
+ save, formatDate, downloadCanvas, exportProject
369
+ };
370
+ }
371
+ }).mount('#app');
372
+ </script>
373
+ </body>
374
+ </html>