fix(tasktree): 恢复父子层级甘特视觉
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import { ChevronLeft, ChevronRight, Maximize2, PanelRight, X } from "lucide-vue-next";
|
||||
import type { TaskTreeReport, TaskTreeTask } from "@/api/tasktree";
|
||||
import ContentViewer from "@/components/common/ContentViewer.vue";
|
||||
import { displayTimeLabel, formatDisplayDateTime } from "@/config/runtime";
|
||||
|
||||
const props = defineProps<{
|
||||
task: TaskTreeTask;
|
||||
@@ -15,6 +16,7 @@ const emit = defineEmits<{
|
||||
close: [];
|
||||
mode: [mode: "dialog" | "docked"];
|
||||
}>();
|
||||
const detailTreeElement = ref<HTMLElement | null>(null);
|
||||
|
||||
const taskIndex = computed(() => props.tasks.findIndex((task) => task.id === props.task.id));
|
||||
const previousTask = computed(() => taskIndex.value > 0 ? props.tasks[taskIndex.value - 1] ?? null : null);
|
||||
@@ -22,6 +24,24 @@ const nextTask = computed(() => taskIndex.value >= 0 ? props.tasks[taskIndex.val
|
||||
const parent = computed(() => props.task.parentId ? props.tasks.find((task) => task.id === props.task.parentId) ?? null : null);
|
||||
const siblings = computed(() => props.tasks.filter((task) => task.parentId === props.task.parentId));
|
||||
const children = computed(() => props.tasks.filter((task) => task.parentId === props.task.id));
|
||||
const localTreeItems = computed(() => {
|
||||
const items: Array<{
|
||||
task: TaskTreeTask;
|
||||
relation: "parent" | "current" | "sibling" | "child";
|
||||
level: number;
|
||||
}> = [];
|
||||
if (parent.value) items.push({ task: parent.value, relation: "parent", level: 0 });
|
||||
for (const sibling of siblings.value) {
|
||||
const current = sibling.id === props.task.id;
|
||||
items.push({ task: sibling, relation: current ? "current" : "sibling", level: parent.value ? 1 : 0 });
|
||||
if (current) {
|
||||
for (const child of children.value) {
|
||||
items.push({ task: child, relation: "child", level: parent.value ? 2 : 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
});
|
||||
const taskReports = computed(() => props.reports.filter((report) => report.taskId === props.task.id));
|
||||
const combinedMarkdown = computed(() => {
|
||||
const description = props.task.description.trim() || "未填写任务说明。";
|
||||
@@ -42,7 +62,10 @@ function plainReportTitle(report: TaskTreeReport) {
|
||||
return report.title.replace(/\[([^\]]+)\]\([^)]+\)/gu, "$1").replace(/`([^`]+)`/gu, "$1").trim();
|
||||
}
|
||||
function kindLabel(kind: TaskTreeTask["kind"]) { return ({ task: "Task", subtask: "Subtask", subsubtask: "Subsubtask" })[kind]; }
|
||||
function dateLabel(value: string | null) { return value ? new Date(value).toLocaleString() : "未设置"; }
|
||||
function relationLabel(relation: "parent" | "current" | "sibling" | "child") {
|
||||
return ({ parent: "父节点", current: "当前任务", sibling: "兄弟任务", child: "子节点" })[relation];
|
||||
}
|
||||
function dateLabel(value: string | null) { return value ? formatDisplayDateTime(value) : "未设置"; }
|
||||
function reportMarkdown(report: TaskTreeReport) {
|
||||
const lines = report.body.replace(/\r\n?/gu, "\n").split("\n");
|
||||
const firstContent = lines.findIndex((line) => line.trim().length > 0);
|
||||
@@ -53,6 +76,19 @@ function reportMarkdown(report: TaskTreeReport) {
|
||||
return lines.join("\n").trimStart();
|
||||
}
|
||||
function normalizeHeading(value: string) { return value.replace(/\s+/gu, "").toLocaleLowerCase(); }
|
||||
async function revealCurrentTask() {
|
||||
await nextTick();
|
||||
const tree = detailTreeElement.value;
|
||||
const row = tree?.querySelector<HTMLElement>("[data-current-task='true']");
|
||||
if (!tree || !row) return;
|
||||
const treeRect = tree.getBoundingClientRect();
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const target = tree.scrollTop + rowRect.top - treeRect.top - tree.clientHeight * 0.18;
|
||||
tree.scrollTop = Math.max(0, Math.min(tree.scrollHeight - tree.clientHeight, target));
|
||||
}
|
||||
|
||||
onMounted(revealCurrentTask);
|
||||
watch(() => props.task.id, revealCurrentTask);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,18 +99,17 @@ function normalizeHeading(value: string) { return value.replace(/\s+/gu, "").toL
|
||||
<button type="button" title="关闭详情" aria-label="关闭详情" @click="emit('close')"><X :size="17" aria-hidden="true" /></button>
|
||||
</header>
|
||||
<div class="tasktree-detail-shell">
|
||||
<aside v-if="mode === 'dialog'" class="tasktree-detail-tree" aria-label="局部任务层级">
|
||||
<aside v-if="mode === 'dialog'" ref="detailTreeElement" class="tasktree-detail-tree" aria-label="局部任务层级">
|
||||
<h3>局部任务</h3>
|
||||
<ol class="tasktree-local-tree">
|
||||
<li v-if="parent" class="level-0 relation-parent">
|
||||
<button type="button" @click="emit('select', parent)"><small>父节点 · {{ kindLabel(parent.kind) }}</small><strong>{{ plainTitle(parent) }}</strong></button>
|
||||
</li>
|
||||
<li v-for="sibling in siblings" :key="sibling.id" :class="[parent ? 'level-1' : 'level-0', { current: sibling.id === task.id }]">
|
||||
<button v-if="sibling.id !== task.id" type="button" @click="emit('select', sibling)"><small>兄弟任务 · {{ kindLabel(sibling.kind) }}</small><strong>{{ plainTitle(sibling) }}</strong></button>
|
||||
<button v-else class="current" type="button" aria-current="true"><small>当前任务 · {{ kindLabel(sibling.kind) }}</small><strong>{{ plainTitle(sibling) }}</strong></button>
|
||||
</li>
|
||||
<li v-for="child in children" :key="child.id" :class="parent ? 'level-2' : 'level-1'">
|
||||
<button type="button" @click="emit('select', child)"><small>子节点 · {{ kindLabel(child.kind) }}</small><strong>{{ plainTitle(child) }}</strong></button>
|
||||
<li
|
||||
v-for="item in localTreeItems"
|
||||
:key="item.task.id"
|
||||
:class="[`level-${item.level}`, `relation-${item.relation}`, { current: item.relation === 'current' }]"
|
||||
:data-current-task="item.relation === 'current' ? 'true' : undefined"
|
||||
>
|
||||
<button v-if="item.relation !== 'current'" type="button" @click="emit('select', item.task)"><small>{{ relationLabel(item.relation) }} · {{ kindLabel(item.task.kind) }}</small><strong>{{ plainTitle(item.task) }}</strong></button>
|
||||
<button v-else class="current" type="button" aria-current="true"><small>{{ relationLabel(item.relation) }} · {{ kindLabel(item.task.kind) }}</small><strong>{{ plainTitle(item.task) }}</strong></button>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
@@ -90,7 +125,7 @@ function normalizeHeading(value: string) { return value.replace(/\s+/gu, "").toL
|
||||
<output>{{ taskIndex + 1 }} / {{ tasks.length }}</output>
|
||||
<button type="button" :disabled="!nextTask" title="下一个任务" aria-label="下一个任务" @click="nextTask && emit('select', nextTask)"><ChevronRight :size="15" aria-hidden="true" /></button>
|
||||
</div>
|
||||
<span>开始 <strong>{{ dateLabel(task.startAt) }}</strong></span><span>截止 <strong>{{ dateLabel(task.dueAt) }}</strong></span><span>状态 <strong>{{ task.status }}</strong></span>
|
||||
<span>开始 <strong>{{ dateLabel(task.startAt) }}</strong></span><span>截止 <strong>{{ dateLabel(task.dueAt) }}</strong></span><span>时区 <strong>{{ displayTimeLabel() }}</strong></span><span>状态 <strong>{{ task.status }}</strong></span>
|
||||
<button v-if="mode === 'dialog'" class="tasktree-detail-dock-button" type="button" title="吸附到右侧" aria-label="吸附到右侧" @click="emit('mode', 'docked')"><PanelRight :size="15" aria-hidden="true" /></button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user