duqing2026 commited on
Commit
3b9326b
·
1 Parent(s): ee8e830

修复 Flask 模板与 Vue 插值冲突,新增健康检查,增强导入与导出功能,补充默认示例数据

Browse files
Files changed (4) hide show
  1. README.md +1 -0
  2. app.py +4 -0
  3. static/js/app.js +102 -9
  4. templates/index.html +10 -12
README.md CHANGED
@@ -6,6 +6,7 @@ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  app_port: 7860
 
9
  ---
10
 
11
  # 商业画布大师 (Lean Canvas Master)
 
6
  sdk: docker
7
  pinned: false
8
  app_port: 7860
9
+ short_description: 商业画布大师
10
  ---
11
 
12
  # 商业画布大师 (Lean Canvas Master)
app.py CHANGED
@@ -10,5 +10,9 @@ def index():
10
  def send_static(path):
11
  return send_from_directory('static', path)
12
 
 
 
 
 
13
  if __name__ == '__main__':
14
  app.run(host='0.0.0.0', port=7860)
 
10
  def send_static(path):
11
  return send_from_directory('static', path)
12
 
13
+ @app.route('/health')
14
+ def health():
15
+ return {'status': 'ok'}, 200
16
+
17
  if __name__ == '__main__':
18
  app.run(host='0.0.0.0', port=7860)
static/js/app.js CHANGED
@@ -12,13 +12,66 @@ const CanvasCell = {
12
  this.$emit('update:items', newItems);
13
  },
14
  updateItem(index, event) {
15
- const newItems = [...this.items];
16
- newItems[index] = event.target.innerText;
 
 
 
 
 
17
  this.$emit('update:items', newItems);
18
  }
19
  }
20
  };
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  const app = Vue.createApp({
23
  components: {
24
  'canvas-cell': CanvasCell
@@ -57,6 +110,9 @@ const app = Vue.createApp({
57
  } catch (e) {
58
  console.error('Failed to load saved data', e);
59
  }
 
 
 
60
  }
61
  },
62
  watch: {
@@ -118,13 +174,16 @@ const app = Vue.createApp({
118
  reader.onload = (e) => {
119
  try {
120
  const data = JSON.parse(e.target.result);
121
- if (data.canvas) {
122
- this.projectTitle = data.projectTitle || '';
123
- this.canvas = data.canvas;
124
- alert('导入成功!');
125
- } else {
126
- alert('无效的文件格式');
127
- }
 
 
 
128
  } catch (err) {
129
  alert('文件解析失败');
130
  }
@@ -152,6 +211,40 @@ const app = Vue.createApp({
152
  console.error('Export failed:', err);
153
  alert('图片导出失败,请重试');
154
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  }
156
  }
157
  });
 
12
  this.$emit('update:items', newItems);
13
  },
14
  updateItem(index, event) {
15
+ const text = (event.target.innerText || '').trim();
16
+ let newItems = [...this.items];
17
+ if (text.length === 0) {
18
+ newItems = newItems.filter((_, i) => i !== index);
19
+ } else {
20
+ newItems[index] = text;
21
+ }
22
  this.$emit('update:items', newItems);
23
  }
24
  }
25
  };
26
 
27
+ const DEFAULT_CANVAS = {
28
+ problem: [
29
+ '客户难以清晰定义问题与目标',
30
+ '缺乏系统化的商业模型梳理工具',
31
+ '方案与市场反馈缺少快速迭代机制'
32
+ ],
33
+ segments: [
34
+ '初创团队与个人创业者',
35
+ '产品经理/运营负责人',
36
+ '创新项目负责人(高校/企业)'
37
+ ],
38
+ uvp: [
39
+ '一体化商业画布工具,所见即所得',
40
+ '支持本地保存与导入导出,隐私友好',
41
+ '快速导出图片与PDF,便于汇报与协作'
42
+ ],
43
+ solution: [
44
+ '提供九宫格画布结构与编辑能力',
45
+ '引导式默认示例,降低上手门槛',
46
+ '支持持久化与分享导出能力'
47
+ ],
48
+ channels: [
49
+ '社媒与社区口碑传播',
50
+ '开发者/创业者社区投放',
51
+ '线下沙龙与培训合作'
52
+ ],
53
+ revenue: [
54
+ '基础版免费,增值版订阅',
55
+ '企业定制授权与部署',
56
+ '培训与咨询服务'
57
+ ],
58
+ cost: [
59
+ '研发与运维费用',
60
+ '宣传与市场费用',
61
+ '内容与培训成本'
62
+ ],
63
+ metrics: [
64
+ '周/日活跃用户',
65
+ '导出次数与分享量',
66
+ '留存与转化率'
67
+ ],
68
+ advantage: [
69
+ '极简本地优先,无外部依赖',
70
+ '中文体验优化与示例指导',
71
+ '轻量架构便于扩展与部署'
72
+ ]
73
+ };
74
+
75
  const app = Vue.createApp({
76
  components: {
77
  'canvas-cell': CanvasCell
 
110
  } catch (e) {
111
  console.error('Failed to load saved data', e);
112
  }
113
+ } else {
114
+ this.projectTitle = '示例项目';
115
+ this.canvas = { ...DEFAULT_CANVAS };
116
  }
117
  },
118
  watch: {
 
174
  reader.onload = (e) => {
175
  try {
176
  const data = JSON.parse(e.target.result);
177
+ const expectedKeys = ['problem','segments','uvp','solution','channels','revenue','cost','metrics','advantage'];
178
+ const canvasObj = (data && typeof data.canvas === 'object') ? data.canvas : {};
179
+ const sanitized = {};
180
+ expectedKeys.forEach(k => {
181
+ const v = canvasObj[k];
182
+ sanitized[k] = Array.isArray(v) ? v.map(x => String(x).trim()).filter(x => x.length > 0) : [];
183
+ });
184
+ this.projectTitle = typeof data.projectTitle === 'string' ? data.projectTitle : '';
185
+ this.canvas = { ...this.canvas, ...sanitized };
186
+ alert('导入成功!');
187
  } catch (err) {
188
  alert('文件解析失败');
189
  }
 
211
  console.error('Export failed:', err);
212
  alert('图片导出失败,请重试');
213
  }
214
+ },
215
+ async exportPDF() {
216
+ const element = document.getElementById('canvas-area');
217
+ try {
218
+ const canvas = await html2canvas(element, {
219
+ scale: 2,
220
+ backgroundColor: this.isDark ? '#1f2937' : '#ffffff',
221
+ useCORS: true
222
+ });
223
+ const imgData = canvas.toDataURL('image/png');
224
+
225
+ const { jsPDF } = window.jspdf || {};
226
+ if (!jsPDF) {
227
+ alert('PDF库加载失败,请稍后重试');
228
+ return;
229
+ }
230
+ const pdf = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' });
231
+ const pageWidth = pdf.internal.pageSize.getWidth();
232
+ const pageHeight = pdf.internal.pageSize.getHeight();
233
+
234
+ const imgWidth = canvas.width;
235
+ const imgHeight = canvas.height;
236
+ const scale = Math.min(pageWidth / imgWidth, pageHeight / imgHeight);
237
+ const renderWidth = imgWidth * scale;
238
+ const renderHeight = imgHeight * scale;
239
+ const offsetX = (pageWidth - renderWidth) / 2;
240
+ const offsetY = (pageHeight - renderHeight) / 2;
241
+
242
+ pdf.addImage(imgData, 'PNG', offsetX, offsetY, renderWidth, renderHeight);
243
+ pdf.save(`lean-canvas-${this.projectTitle || 'untitled'}.pdf`);
244
+ } catch (err) {
245
+ console.error('PDF export failed:', err);
246
+ alert('PDF导出失败,请重试');
247
+ }
248
  }
249
  }
250
  });
templates/index.html CHANGED
@@ -53,6 +53,7 @@
53
  </style>
54
  </head>
55
  <body class="bg-gray-50 text-gray-800 dark:bg-gray-900 dark:text-gray-100 transition-colors duration-300">
 
56
  <div id="app" class="min-h-screen flex flex-col">
57
  <!-- Header -->
58
  <header class="bg-white dark:bg-gray-800 shadow-sm p-4 sticky top-0 z-50">
@@ -67,19 +68,22 @@
67
  <i :class="isDark ? 'fa-solid fa-sun text-yellow-400' : 'fa-solid fa-moon text-gray-600'"></i>
68
  </button>
69
  <div class="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-1"></div>
70
- <button @click="clearCanvas" class="btn-secondary">
71
  <i class="fa-solid fa-trash-can mr-1"></i> 清空
72
  </button>
73
- <button @click="saveJSON" class="btn-secondary">
74
  <i class="fa-solid fa-download mr-1"></i> 保存JSON
75
  </button>
76
- <label class="btn-secondary cursor-pointer">
77
  <i class="fa-solid fa-upload mr-1"></i> 导入JSON
78
  <input type="file" @change="loadJSON" accept=".json" class="hidden">
79
  </label>
80
- <button @click="exportImage" class="btn-primary bg-blue-600 text-white hover:bg-blue-700">
81
  <i class="fa-solid fa-image mr-1"></i> 导出图片
82
  </button>
 
 
 
83
  </div>
84
  </div>
85
  </header>
@@ -221,15 +225,9 @@
221
  </div>
222
  </script>
223
 
224
- <style>
225
- .btn-secondary {
226
- @apply px-3 py-1.5 rounded text-sm border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition flex items-center;
227
- }
228
- .btn-primary {
229
- @apply px-3 py-1.5 rounded text-sm flex items-center transition shadow-sm;
230
- }
231
- </style>
232
 
233
  <script src="/static/js/app.js"></script>
 
234
  </body>
235
  </html>
 
53
  </style>
54
  </head>
55
  <body class="bg-gray-50 text-gray-800 dark:bg-gray-900 dark:text-gray-100 transition-colors duration-300">
56
+ {% raw %}
57
  <div id="app" class="min-h-screen flex flex-col">
58
  <!-- Header -->
59
  <header class="bg-white dark:bg-gray-800 shadow-sm p-4 sticky top-0 z-50">
 
68
  <i :class="isDark ? 'fa-solid fa-sun text-yellow-400' : 'fa-solid fa-moon text-gray-600'"></i>
69
  </button>
70
  <div class="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-1"></div>
71
+ <button @click="clearCanvas" class="px-3 py-1.5 rounded text-sm border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition flex items-center">
72
  <i class="fa-solid fa-trash-can mr-1"></i> 清空
73
  </button>
74
+ <button @click="saveJSON" class="px-3 py-1.5 rounded text-sm border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition flex items-center">
75
  <i class="fa-solid fa-download mr-1"></i> 保存JSON
76
  </button>
77
+ <label class="px-3 py-1.5 rounded text-sm border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition flex items-center cursor-pointer">
78
  <i class="fa-solid fa-upload mr-1"></i> 导入JSON
79
  <input type="file" @change="loadJSON" accept=".json" class="hidden">
80
  </label>
81
+ <button @click="exportImage" class="px-3 py-1.5 rounded text-sm flex items-center transition shadow-sm bg-blue-600 text-white hover:bg-blue-700">
82
  <i class="fa-solid fa-image mr-1"></i> 导出图片
83
  </button>
84
+ <button @click="exportPDF" class="px-3 py-1.5 rounded text-sm flex items-center transition shadow-sm bg-green-600 text-white hover:bg-green-700">
85
+ <i class="fa-solid fa-file-pdf mr-1"></i> 导出PDF
86
+ </button>
87
  </div>
88
  </div>
89
  </header>
 
225
  </div>
226
  </script>
227
 
228
+
 
 
 
 
 
 
 
229
 
230
  <script src="/static/js/app.js"></script>
231
+ {% endraw %}
232
  </body>
233
  </html>