duqing2026 commited on
Commit
d51c807
·
1 Parent(s): 86afe7c

优化升级

Browse files
Files changed (1) hide show
  1. templates/index.html +79 -25
templates/index.html CHANGED
@@ -166,14 +166,14 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
166
  <textarea
167
  v-if="!isPreviewMode"
168
  v-model="item.title"
169
- class="w-full text-sm font-medium text-gray-800 bg-transparent resize-none focus:outline-none mb-1 overflow-hidden"
170
  rows="2"
171
  placeholder="输入任务内容..."
172
  @input="autoResize($event.target)"
173
  @focus="autoResize($event.target)"
174
  :ref="el => { if(el) autoResize(el) }"
175
  ></textarea>
176
- <div v-else class="text-sm font-medium text-gray-800 mb-1 whitespace-pre-wrap break-words leading-relaxed">{{ item.title }}</div>
177
 
178
  <!-- Item Controls (Hover) -->
179
  <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">
@@ -315,27 +315,49 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
315
 
316
  // Persistence: Load from Server -> Fallback to LocalStorage
317
  const loadData = async () => {
 
318
  try {
319
  const res = await fetch('/api/data');
320
  const data = await res.json();
321
- if (data) {
322
- columns.value = data;
323
  console.log('Loaded from server');
324
- return;
325
  }
326
  } catch (e) {
327
  console.log('Server load failed, checking local storage');
328
  }
329
 
330
- const saved = localStorage.getItem('roadmap_data');
331
- if (saved) {
332
- try {
333
- columns.value = JSON.parse(saved);
334
- } catch(e) {
335
- columns.value = JSON.parse(JSON.stringify(defaultColumns));
 
 
 
 
 
336
  }
337
- } else {
338
- columns.value = JSON.parse(JSON.stringify(defaultColumns));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  }
340
  };
341
 
@@ -386,11 +408,16 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
386
 
387
  const onColDrop = (e, dropIndex) => {
388
  if (isPreviewMode.value) return;
389
- const type = e.dataTransfer.getData('type');
390
- if (type === 'col' && dragCol.value !== null && dragCol.value !== dropIndex) {
 
391
  const item = columns.value[dragCol.value];
392
- columns.value.splice(dragCol.value, 1);
393
- columns.value.splice(dropIndex, 0, item);
 
 
 
 
394
  }
395
  dragCol.value = null;
396
  };
@@ -400,7 +427,9 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
400
  dragItem.value = { colIndex, itemIndex };
401
  e.dataTransfer.effectAllowed = 'move';
402
  e.dataTransfer.setData('type', 'item');
 
403
  e.target.classList.add('opacity-50');
 
404
  };
405
 
406
  const onItemDragEnd = (e) => {
@@ -409,6 +438,9 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
409
  document.querySelectorAll('.drop-indicator-top, .drop-indicator-bottom').forEach(el => {
410
  el.classList.remove('drop-indicator-top', 'drop-indicator-bottom');
411
  });
 
 
 
412
  dragItem.value = null;
413
  };
414
 
@@ -441,11 +473,14 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
441
  e.currentTarget.classList.remove('drop-indicator-top', 'drop-indicator-bottom');
442
  }
443
 
444
- const type = e.dataTransfer.getData('type');
445
-
446
- if (type === 'item' && dragItem.value) {
447
  const { colIndex: srcColIndex, itemIndex: srcItemIndex } = dragItem.value;
448
- const item = columns.value[srcColIndex].items[srcItemIndex];
 
 
 
 
449
 
450
  // Calculate insertion index
451
  let insertIndex = dropItemIndex;
@@ -461,7 +496,7 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
461
  }
462
 
463
  // Remove from source
464
- columns.value[srcColIndex].items.splice(srcItemIndex, 1);
465
 
466
  // Adjust index if moving within same list
467
  if (srcColIndex === dropColIndex) {
@@ -470,15 +505,24 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
470
  }
471
  }
472
 
473
- // Handle drop on empty column
474
  if (dropItemIndex === -1) {
475
- insertIndex = columns.value[dropColIndex].items.length;
 
476
  }
477
 
478
  // Insert into target
479
- columns.value[dropColIndex].items.splice(insertIndex, 0, item);
 
 
 
 
 
 
480
 
481
  dragItem.value = null;
 
 
482
  }
483
  };
484
 
@@ -596,6 +640,16 @@ <h1 class="text-xl font-bold text-gray-900">产品路线图生成器 <span v-if=
596
  return {
597
  columns,
598
  roadmapContainer,
 
 
 
 
 
 
 
 
 
 
599
  getTagColor,
600
  cycleTag,
601
  addItem,
 
166
  <textarea
167
  v-if="!isPreviewMode"
168
  v-model="item.title"
169
+ class="w-full text-sm font-medium text-gray-800 bg-transparent resize-none focus:outline-none mb-1 overflow-hidden min-h-[2.5rem]"
170
  rows="2"
171
  placeholder="输入任务内容..."
172
  @input="autoResize($event.target)"
173
  @focus="autoResize($event.target)"
174
  :ref="el => { if(el) autoResize(el) }"
175
  ></textarea>
176
+ <div v-else class="text-sm font-medium text-gray-800 mb-1 whitespace-pre-wrap break-words leading-relaxed min-h-[1.5rem]">{{ item.title || '(无内容)' }}</div>
177
 
178
  <!-- Item Controls (Hover) -->
179
  <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">
 
315
 
316
  // Persistence: Load from Server -> Fallback to LocalStorage
317
  const loadData = async () => {
318
+ let loadedData = null;
319
  try {
320
  const res = await fetch('/api/data');
321
  const data = await res.json();
322
+ if (data && Array.isArray(data) && data.length > 0) {
323
+ loadedData = data;
324
  console.log('Loaded from server');
 
325
  }
326
  } catch (e) {
327
  console.log('Server load failed, checking local storage');
328
  }
329
 
330
+ if (!loadedData) {
331
+ const saved = localStorage.getItem('roadmap_data');
332
+ if (saved) {
333
+ try {
334
+ const parsed = JSON.parse(saved);
335
+ if (Array.isArray(parsed) && parsed.length > 0) {
336
+ loadedData = parsed;
337
+ }
338
+ } catch(e) {
339
+ console.error('Local storage parse error', e);
340
+ }
341
  }
342
+ }
343
+
344
+ // Fallback to default
345
+ if (!loadedData) {
346
+ loadedData = JSON.parse(JSON.stringify(defaultColumns));
347
+ }
348
+
349
+ // Validate and Repair Data
350
+ // Ensure every item has a title field, even if empty string
351
+ if (loadedData) {
352
+ loadedData.forEach(col => {
353
+ if (!col.items) col.items = [];
354
+ col.items.forEach(item => {
355
+ if (item.title === undefined || item.title === null) {
356
+ item.title = '新任务'; // Repair missing title
357
+ }
358
+ });
359
+ });
360
+ columns.value = loadedData;
361
  }
362
  };
363
 
 
408
 
409
  const onColDrop = (e, dropIndex) => {
410
  if (isPreviewMode.value) return;
411
+
412
+ // Relaxed check: trust internal state primarily
413
+ if (dragCol.value !== null && dragCol.value !== dropIndex) {
414
  const item = columns.value[dragCol.value];
415
+ // Using explicit array manipulation for reactivity
416
+ const newColumns = [...columns.value];
417
+ newColumns.splice(dragCol.value, 1);
418
+ newColumns.splice(dropIndex, 0, item);
419
+ columns.value = newColumns;
420
+ console.log('Column moved:', dragCol.value, '->', dropIndex);
421
  }
422
  dragCol.value = null;
423
  };
 
427
  dragItem.value = { colIndex, itemIndex };
428
  e.dataTransfer.effectAllowed = 'move';
429
  e.dataTransfer.setData('type', 'item');
430
+ e.dataTransfer.setData('text/plain', JSON.stringify({ colIndex, itemIndex })); // Fallback
431
  e.target.classList.add('opacity-50');
432
+ console.log('Drag Start:', colIndex, itemIndex);
433
  };
434
 
435
  const onItemDragEnd = (e) => {
 
438
  document.querySelectorAll('.drop-indicator-top, .drop-indicator-bottom').forEach(el => {
439
  el.classList.remove('drop-indicator-top', 'drop-indicator-bottom');
440
  });
441
+ // Do NOT nullify dragItem here immediately if we want to debug,
442
+ // but for logic safety, we keep it.
443
+ // The drop event happens BEFORE dragEnd, so this is safe.
444
  dragItem.value = null;
445
  };
446
 
 
473
  e.currentTarget.classList.remove('drop-indicator-top', 'drop-indicator-bottom');
474
  }
475
 
476
+ // Relaxed check: trust internal state primarily
477
+ if (dragItem.value) {
 
478
  const { colIndex: srcColIndex, itemIndex: srcItemIndex } = dragItem.value;
479
+ console.log('Drop detected:', srcColIndex, srcItemIndex, '->', dropColIndex, dropItemIndex);
480
+
481
+ // Deep clone to avoid reference issues during splice
482
+ const newColumns = JSON.parse(JSON.stringify(columns.value));
483
+ const item = newColumns[srcColIndex].items[srcItemIndex];
484
 
485
  // Calculate insertion index
486
  let insertIndex = dropItemIndex;
 
496
  }
497
 
498
  // Remove from source
499
+ newColumns[srcColIndex].items.splice(srcItemIndex, 1);
500
 
501
  // Adjust index if moving within same list
502
  if (srcColIndex === dropColIndex) {
 
505
  }
506
  }
507
 
508
+ // Handle drop on empty column or specific logic
509
  if (dropItemIndex === -1) {
510
+ // Dropped on container, append to end
511
+ insertIndex = newColumns[dropColIndex].items.length;
512
  }
513
 
514
  // Insert into target
515
+ // Ensure insertIndex is within bounds [0, length]
516
+ insertIndex = Math.max(0, Math.min(insertIndex, newColumns[dropColIndex].items.length));
517
+
518
+ newColumns[dropColIndex].items.splice(insertIndex, 0, item);
519
+
520
+ // Update state
521
+ columns.value = newColumns;
522
 
523
  dragItem.value = null;
524
+ } else {
525
+ console.warn('Drop ignored: no dragItem value');
526
  }
527
  };
528
 
 
640
  return {
641
  columns,
642
  roadmapContainer,
643
+ isPreviewMode,
644
+ completionRate,
645
+ onColDragStart,
646
+ onColDragEnd,
647
+ onColDrop,
648
+ onItemDragStart,
649
+ onItemDragEnd,
650
+ onItemDragOver,
651
+ onItemDragLeave,
652
+ onItemDrop,
653
  getTagColor,
654
  cycleTag,
655
  addItem,