Spaces:
Sleeping
Sleeping
Commit ·
d51c807
1
Parent(s): 86afe7c
优化升级
Browse files- 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 |
-
|
| 323 |
console.log('Loaded from server');
|
| 324 |
-
return;
|
| 325 |
}
|
| 326 |
} catch (e) {
|
| 327 |
console.log('Server load failed, checking local storage');
|
| 328 |
}
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
-
}
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 390 |
-
|
|
|
|
| 391 |
const item = columns.value[dragCol.value];
|
| 392 |
-
|
| 393 |
-
columns.value
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 445 |
-
|
| 446 |
-
if (type === 'item' && dragItem.value) {
|
| 447 |
const { colIndex: srcColIndex, itemIndex: srcItemIndex } = dragItem.value;
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 476 |
}
|
| 477 |
|
| 478 |
// Insert into target
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|