duqing2026 commited on
Commit
06a09a4
·
1 Parent(s): 2426b67

Enhance: Fix export image issue, add Drag and Drop, and improve UI

Browse files
Files changed (2) hide show
  1. README.md +1 -0
  2. templates/index.html +149 -33
README.md CHANGED
@@ -6,6 +6,7 @@ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  app_port: 7860
 
9
  ---
10
 
11
  # 产品路线图生成器 (Roadmap Builder Pro)
 
6
  sdk: docker
7
  pinned: false
8
  app_port: 7860
9
+ short_description: 产品路线图生成器
10
  ---
11
 
12
  # 产品路线图生成器 (Roadmap Builder Pro)
templates/index.html CHANGED
@@ -31,11 +31,28 @@
31
  .exporting .no-export {
32
  display: none !important;
33
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  </style>
35
  </head>
36
  <body class="bg-gray-50 text-gray-800 h-screen flex flex-col overflow-hidden">
37
  <div id="app" class="flex flex-col h-full">
38
- <!-- Header -->
39
  <header class="bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center shrink-0 z-10 shadow-sm">
40
  <div class="flex items-center gap-3">
41
  <div class="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center text-white text-xl">
@@ -43,11 +60,24 @@
43
  </div>
44
  <div>
45
  <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
46
- <p class="text-xs text-gray-500">简单、高效的 Roadmap 规划工具</p>
 
 
 
 
 
47
  </div>
48
  </div>
49
  <div class="flex items-center gap-3">
50
- <button @click="resetData" class="px-4 py-2 text-sm text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors">
 
 
 
 
 
 
 
 
51
  <i class="fa-solid fa-rotate-right mr-2"></i>重置
52
  </button>
53
  <button @click="exportImage" class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition-colors shadow-sm flex items-center">
@@ -61,28 +91,48 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
61
  <div class="flex h-full gap-6 pb-4" :class="{ 'pr-6': true }">
62
 
63
  <!-- Columns -->
64
- <div v-for="(col, colIndex) in columns" :key="col.id" class="w-80 flex flex-col shrink-0 bg-gray-100 rounded-xl border border-gray-200/60 shadow-sm h-full max-h-full">
 
 
 
 
 
 
 
 
65
  <!-- Column Header -->
66
- <div class="p-4 border-b border-gray-200/60 bg-gray-50/50 rounded-t-xl flex justify-between items-start group">
67
  <div class="w-full">
68
- <input v-model="col.title" class="w-full bg-transparent font-bold text-gray-700 focus:outline-none focus:border-b-2 focus:border-indigo-500 transition-colors px-1 py-0.5" placeholder="阶段名称">
 
69
  <div class="text-xs text-gray-400 mt-1 px-1">{{ col.items.length }} 个任务</div>
70
  </div>
71
- <button @click="removeColumn(colIndex)" class="text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity ml-2 no-export" v-if="columns.length > 1">
72
  <i class="fa-solid fa-trash"></i>
73
  </button>
74
  </div>
75
 
76
  <!-- Items List -->
77
- <div class="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar">
78
- <div v-for="(item, itemIndex) in col.items" :key="item.id" class="bg-white p-3 rounded-lg shadow-sm border border-gray-200 group hover:shadow-md transition-shadow relative">
 
 
 
 
 
 
 
 
 
 
 
79
  <!-- Item Tags -->
80
  <div class="flex flex-wrap gap-1 mb-2">
81
  <span
82
  class="px-2 py-0.5 rounded text-[10px] font-medium cursor-pointer select-none"
83
  :class="getTagColor(item.tag)"
84
- @click="cycleTag(item)"
85
- title="点击切换标签颜色"
86
  >
87
  {{ item.tag || '标签' }}
88
  </span>
@@ -90,6 +140,7 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
90
 
91
  <!-- Item Content -->
92
  <textarea
 
93
  v-model="item.title"
94
  class="w-full text-sm font-medium text-gray-800 bg-transparent resize-none focus:outline-none mb-1 overflow-hidden"
95
  rows="2"
@@ -98,9 +149,10 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
98
  @focus="autoResize($event.target)"
99
  :ref="el => { if(el) autoResize(el) }"
100
  ></textarea>
 
101
 
102
  <!-- Item Controls (Hover) -->
103
- <div class="absolute top-2 right-2 flex flex-col gap-1 opacity-0 group-hover:opacity-100 transition-opacity bg-white/90 rounded shadow-sm no-export">
104
  <button @click="moveItem(colIndex, itemIndex, 'up')" class="w-6 h-6 flex items-center justify-center text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 rounded" :disabled="itemIndex === 0">
105
  <i class="fa-solid fa-chevron-up text-xs"></i>
106
  </button>
@@ -111,29 +163,16 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
111
  <i class="fa-solid fa-xmark text-xs"></i>
112
  </button>
113
  </div>
114
-
115
- <!-- Move Column Controls -->
116
- <div class="flex justify-between mt-2 pt-2 border-t border-gray-100 no-export">
117
- <button
118
- @click="moveItemAcross(colIndex, itemIndex, -1)"
119
- class="text-xs text-gray-400 hover:text-indigo-600 disabled:opacity-30"
120
- :disabled="colIndex === 0"
121
- >
122
- <i class="fa-solid fa-arrow-left"></i>
123
- </button>
124
- <button
125
- @click="moveItemAcross(colIndex, itemIndex, 1)"
126
- class="text-xs text-gray-400 hover:text-indigo-600 disabled:opacity-30"
127
- :disabled="colIndex === columns.length - 1"
128
- >
129
- <i class="fa-solid fa-arrow-right"></i>
130
- </button>
131
- </div>
132
  </div>
133
  </div>
134
 
135
  <!-- Add Item Button -->
136
- <div class="p-3 border-t border-gray-200/60 no-export">
137
  <button @click="addItem(colIndex)" class="w-full py-2 border border-dashed border-gray-300 rounded-lg text-gray-500 hover:text-indigo-600 hover:border-indigo-400 hover:bg-indigo-50 transition-all text-sm flex items-center justify-center gap-2">
138
  <i class="fa-solid fa-plus"></i> 添加任务
139
  </button>
@@ -141,7 +180,7 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
141
  </div>
142
 
143
  <!-- Add Column Button -->
144
- <div class="w-12 shrink-0 flex items-center justify-center no-export">
145
  <button @click="addColumn" class="w-10 h-10 bg-white rounded-full shadow border border-gray-200 text-gray-500 hover:text-indigo-600 hover:border-indigo-500 transition-all flex items-center justify-center" title="添加新阶段">
146
  <i class="fa-solid fa-plus text-lg"></i>
147
  </button>
@@ -172,11 +211,14 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
172
  </div>
173
 
174
  <script>
175
- const { createApp, ref, onMounted, watch } = Vue;
176
 
177
  createApp({
178
  setup() {
179
  const roadmapContainer = ref(null);
 
 
 
180
 
181
  // Initial Data
182
  const defaultColumns = [
@@ -245,11 +287,76 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
245
  }
246
  });
247
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  // Auto save
249
  watch(columns, (newVal) => {
250
  localStorage.setItem('roadmap_data', JSON.stringify(newVal));
251
  }, { deep: true });
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  const getTagColor = (tag) => {
254
  return tagStyles[tag] || 'bg-gray-100 text-gray-600 border border-gray-200';
255
  };
@@ -321,6 +428,11 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
321
  const exportImage = async () => {
322
  if (!roadmapContainer.value) return;
323
 
 
 
 
 
 
324
  // Add class to hide buttons
325
  document.body.classList.add('exporting');
326
 
@@ -349,6 +461,10 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
349
  alert('导出失败,请重试');
350
  } finally {
351
  document.body.classList.remove('exporting');
 
 
 
 
352
  }
353
  };
354
 
 
31
  .exporting .no-export {
32
  display: none !important;
33
  }
34
+
35
+ /* Export Optimization: Expand all scrollable areas */
36
+ .exporting .roadmap-scroll {
37
+ overflow: visible !important;
38
+ height: auto !important;
39
+ width: max-content !important; /* Ensure full width */
40
+ }
41
+ .exporting .overflow-y-auto {
42
+ overflow: visible !important;
43
+ height: auto !important;
44
+ }
45
+ .exporting .h-full {
46
+ height: auto !important;
47
+ }
48
+ .exporting .max-h-full {
49
+ max-height: none !important;
50
+ }
51
  </style>
52
  </head>
53
  <body class="bg-gray-50 text-gray-800 h-screen flex flex-col overflow-hidden">
54
  <div id="app" class="flex flex-col h-full">
55
+ <!-- Header -->
56
  <header class="bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center shrink-0 z-10 shadow-sm">
57
  <div class="flex items-center gap-3">
58
  <div class="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center text-white text-xl">
 
60
  </div>
61
  <div>
62
  <h1 class="text-xl font-bold text-gray-900">产品路线图生成器</h1>
63
+ <div class="flex items-center gap-2 mt-0.5">
64
+ <div class="h-1.5 w-24 bg-gray-200 rounded-full overflow-hidden">
65
+ <div class="h-full bg-green-500 transition-all duration-500" :style="{ width: completionRate + '%' }"></div>
66
+ </div>
67
+ <span class="text-xs text-gray-500">{{ completionRate }}% 完成</span>
68
+ </div>
69
  </div>
70
  </div>
71
  <div class="flex items-center gap-3">
72
+ <div class="flex bg-gray-100 p-1 rounded-lg mr-2">
73
+ <button @click="isPreviewMode = false" class="px-3 py-1 text-xs font-medium rounded-md transition-all" :class="!isPreviewMode ? 'bg-white text-indigo-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'">
74
+ <i class="fa-solid fa-pen mr-1"></i>编辑
75
+ </button>
76
+ <button @click="isPreviewMode = true" class="px-3 py-1 text-xs font-medium rounded-md transition-all" :class="isPreviewMode ? 'bg-white text-indigo-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'">
77
+ <i class="fa-solid fa-eye mr-1"></i>预览
78
+ </button>
79
+ </div>
80
+ <button @click="resetData" class="px-4 py-2 text-sm text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" v-if="!isPreviewMode">
81
  <i class="fa-solid fa-rotate-right mr-2"></i>重置
82
  </button>
83
  <button @click="exportImage" class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition-colors shadow-sm flex items-center">
 
91
  <div class="flex h-full gap-6 pb-4" :class="{ 'pr-6': true }">
92
 
93
  <!-- Columns -->
94
+ <div
95
+ v-for="(col, colIndex) in columns"
96
+ :key="col.id"
97
+ class="w-80 flex flex-col shrink-0 bg-gray-100 rounded-xl border border-gray-200/60 shadow-sm h-full max-h-full transition-all duration-200"
98
+ :draggable="!isPreviewMode"
99
+ @dragstart="onColDragStart($event, colIndex)"
100
+ @dragover.prevent
101
+ @drop="onColDrop($event, colIndex)"
102
+ >
103
  <!-- Column Header -->
104
+ <div class="p-4 border-b border-gray-200/60 bg-gray-50/50 rounded-t-xl flex justify-between items-start group cursor-move">
105
  <div class="w-full">
106
+ <input v-if="!isPreviewMode" v-model="col.title" class="w-full bg-transparent font-bold text-gray-700 focus:outline-none focus:border-b-2 focus:border-indigo-500 transition-colors px-1 py-0.5" placeholder="阶段名称">
107
+ <div v-else class="font-bold text-gray-700 px-1 py-0.5">{{ col.title }}</div>
108
  <div class="text-xs text-gray-400 mt-1 px-1">{{ col.items.length }} 个任务</div>
109
  </div>
110
+ <button @click="removeColumn(colIndex)" class="text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity ml-2 no-export" v-if="columns.length > 1 && !isPreviewMode">
111
  <i class="fa-solid fa-trash"></i>
112
  </button>
113
  </div>
114
 
115
  <!-- Items List -->
116
+ <div
117
+ class="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar"
118
+ @dragover.prevent
119
+ @drop="onItemDrop($event, colIndex, -1)"
120
+ >
121
+ <div
122
+ v-for="(item, itemIndex) in col.items"
123
+ :key="item.id"
124
+ class="bg-white p-3 rounded-lg shadow-sm border border-gray-200 group hover:shadow-md transition-shadow relative cursor-move"
125
+ :draggable="!isPreviewMode"
126
+ @dragstart="onItemDragStart($event, colIndex, itemIndex)"
127
+ @drop.stop="onItemDrop($event, colIndex, itemIndex)"
128
+ >
129
  <!-- Item Tags -->
130
  <div class="flex flex-wrap gap-1 mb-2">
131
  <span
132
  class="px-2 py-0.5 rounded text-[10px] font-medium cursor-pointer select-none"
133
  :class="getTagColor(item.tag)"
134
+ @click="!isPreviewMode && cycleTag(item)"
135
+ :title="!isPreviewMode ? '点击切换标签颜色' : ''"
136
  >
137
  {{ item.tag || '标签' }}
138
  </span>
 
140
 
141
  <!-- Item Content -->
142
  <textarea
143
+ v-if="!isPreviewMode"
144
  v-model="item.title"
145
  class="w-full text-sm font-medium text-gray-800 bg-transparent resize-none focus:outline-none mb-1 overflow-hidden"
146
  rows="2"
 
149
  @focus="autoResize($event.target)"
150
  :ref="el => { if(el) autoResize(el) }"
151
  ></textarea>
152
+ <div v-else class="text-sm font-medium text-gray-800 mb-1 whitespace-pre-wrap break-words leading-relaxed">{{ item.title }}</div>
153
 
154
  <!-- Item Controls (Hover) -->
155
+ <div class="absolute top-2 right-2 flex flex-col gap-1 opacity-0 group-hover:opacity-100 transition-opacity bg-white/90 rounded shadow-sm no-export" v-if="!isPreviewMode">
156
  <button @click="moveItem(colIndex, itemIndex, 'up')" class="w-6 h-6 flex items-center justify-center text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 rounded" :disabled="itemIndex === 0">
157
  <i class="fa-solid fa-chevron-up text-xs"></i>
158
  </button>
 
163
  <i class="fa-solid fa-xmark text-xs"></i>
164
  </button>
165
  </div>
166
+ </div>
167
+
168
+ <!-- Empty State Drop Zone Hint -->
169
+ <div v-if="col.items.length === 0 && !isPreviewMode" class="h-20 border-2 border-dashed border-gray-200 rounded-lg flex items-center justify-center text-gray-300 text-xs">
170
+ 拖拽任务到此处
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </div>
172
  </div>
173
 
174
  <!-- Add Item Button -->
175
+ <div class="p-3 border-t border-gray-200/60 no-export" v-if="!isPreviewMode">
176
  <button @click="addItem(colIndex)" class="w-full py-2 border border-dashed border-gray-300 rounded-lg text-gray-500 hover:text-indigo-600 hover:border-indigo-400 hover:bg-indigo-50 transition-all text-sm flex items-center justify-center gap-2">
177
  <i class="fa-solid fa-plus"></i> 添加任务
178
  </button>
 
180
  </div>
181
 
182
  <!-- Add Column Button -->
183
+ <div class="w-12 shrink-0 flex items-center justify-center no-export" v-if="!isPreviewMode">
184
  <button @click="addColumn" class="w-10 h-10 bg-white rounded-full shadow border border-gray-200 text-gray-500 hover:text-indigo-600 hover:border-indigo-500 transition-all flex items-center justify-center" title="添加新阶段">
185
  <i class="fa-solid fa-plus text-lg"></i>
186
  </button>
 
211
  </div>
212
 
213
  <script>
214
+ const { createApp, ref, onMounted, watch, computed, nextTick } = Vue;
215
 
216
  createApp({
217
  setup() {
218
  const roadmapContainer = ref(null);
219
+ const isPreviewMode = ref(false);
220
+ const dragItem = ref(null);
221
+ const dragCol = ref(null);
222
 
223
  // Initial Data
224
  const defaultColumns = [
 
287
  }
288
  });
289
 
290
+ const completionRate = computed(() => {
291
+ let total = 0;
292
+ let completed = 0;
293
+ columns.value.forEach(col => {
294
+ col.items.forEach(item => {
295
+ total++;
296
+ if (item.tag === '已完成') completed++;
297
+ });
298
+ });
299
+ return total === 0 ? 0 : Math.round((completed / total) * 100);
300
+ });
301
+
302
  // Auto save
303
  watch(columns, (newVal) => {
304
  localStorage.setItem('roadmap_data', JSON.stringify(newVal));
305
  }, { deep: true });
306
 
307
+ // Drag and Drop Handlers
308
+ const onColDragStart = (e, colIndex) => {
309
+ if (isPreviewMode.value) return;
310
+ dragCol.value = colIndex;
311
+ e.dataTransfer.effectAllowed = 'move';
312
+ e.dataTransfer.setData('type', 'col');
313
+ // Transparent drag image hack if needed, but default is usually fine
314
+ };
315
+
316
+ const onColDrop = (e, dropIndex) => {
317
+ if (isPreviewMode.value) return;
318
+ const type = e.dataTransfer.getData('type');
319
+ if (type === 'col' && dragCol.value !== null && dragCol.value !== dropIndex) {
320
+ const item = columns.value[dragCol.value];
321
+ columns.value.splice(dragCol.value, 1);
322
+ columns.value.splice(dropIndex, 0, item);
323
+ dragCol.value = null;
324
+ }
325
+ };
326
+
327
+ const onItemDragStart = (e, colIndex, itemIndex) => {
328
+ if (isPreviewMode.value) return;
329
+ dragItem.value = { colIndex, itemIndex };
330
+ e.dataTransfer.effectAllowed = 'move';
331
+ e.dataTransfer.setData('type', 'item');
332
+ };
333
+
334
+ const onItemDrop = (e, dropColIndex) => {
335
+ if (isPreviewMode.value) return;
336
+ const type = e.dataTransfer.getData('type');
337
+
338
+ if (type === 'item' && dragItem.value) {
339
+ const { colIndex: srcColIndex, itemIndex: srcItemIndex } = dragItem.value;
340
+
341
+ // Don't do anything if dropping in same place (logic simplified for list append)
342
+ // Advanced: Calculate exact drop index based on Y position.
343
+ // For now, appending to the column is good enough for MVP DnD.
344
+
345
+ if (srcColIndex === dropColIndex) {
346
+ // Reordering within same column (simplified: move to end if dropped on container)
347
+ // Ideally we'd find the index, but let's stick to simple "move to another column" or "reorder cols"
348
+ // If user wants to reorder items in same column, they need to drop ON an item, which is harder to implement in one go.
349
+ // Let's implement "drop on column adds to end" logic first.
350
+ }
351
+
352
+ const item = columns.value[srcColIndex].items[srcItemIndex];
353
+ columns.value[srcColIndex].items.splice(srcItemIndex, 1);
354
+ columns.value[dropColIndex].items.push(item);
355
+
356
+ dragItem.value = null;
357
+ }
358
+ };
359
+
360
  const getTagColor = (tag) => {
361
  return tagStyles[tag] || 'bg-gray-100 text-gray-600 border border-gray-200';
362
  };
 
428
  const exportImage = async () => {
429
  if (!roadmapContainer.value) return;
430
 
431
+ // Auto-switch to Preview Mode for better export quality
432
+ const wasPreview = isPreviewMode.value;
433
+ isPreviewMode.value = true;
434
+ await nextTick();
435
+
436
  // Add class to hide buttons
437
  document.body.classList.add('exporting');
438
 
 
461
  alert('导出失败,请重试');
462
  } finally {
463
  document.body.classList.remove('exporting');
464
+ // Restore original mode if needed
465
+ if (!wasPreview) {
466
+ isPreviewMode.value = false;
467
+ }
468
  }
469
  };
470