Merge pull request #2254 from pikasTech/fix/2245-mdtodo-dedupe-jd01

fix: dedupe MDTODO page context labels
This commit is contained in:
Lyon
2026-06-29 20:49:46 +08:00
committed by GitHub
5 changed files with 38 additions and 51 deletions
@@ -112,8 +112,6 @@ const reportPaneOpen = computed(() => Boolean(report.reportPreview.value));
const workbenchLaunchEnabled = computed(() => launch.isLaunchEnabled(source.navigation.value));
const launchButtonDisabled = computed(() => !selectedTask.value || !workbenchLaunchEnabled.value || launch.launchLoading.value);
const selectedSourceCanReindex = computed(() => Boolean(selectedSourceId.value && !taskData.taskLoading.value && !source.sourceReindexLoading.value));
const selectedTaskLabel = computed(() => selectedTask.value ? `${selectedTask.value.taskId || ''} ${selectedTask.value.title || ''}`.trim() : "");
function reportLinksOnly(links: MdtodoTaskLinkRecord[]): MdtodoTaskLinkRecord[] {
return links.filter((link) => link.kind === "markdown-report");
}
@@ -398,17 +396,6 @@ function setError(err: unknown): void {
<template>
<section class="route-stack mdtodo-page" data-testid="project-management-mdtodo">
<header class="mdtodo-framebar">
<div class="mdtodo-title-lockup">
<span>Project Management</span>
<strong>MDTODO</strong>
</div>
<div class="mdtodo-route-chip">
<span>{{ selectedFileName }}</span>
<small v-if="selectedTask">{{ selectedTask.taskId }} · {{ selectedTask.title }}</small>
</div>
</header>
<LoadingState v-if="loading && !source.sources.value.length" class="mdtodo-route-loading" label="加载 MDTODO source" />
<section v-else-if="error" class="data-panel mdtodo-error" data-testid="project-management-error">
<EmptyState title="MDTODO 加载失败" :description="error" :api-error="apiError" :diagnostic="diagnostic" action-label="重试" @action="loadPage" />
@@ -422,8 +409,6 @@ function setError(err: unknown): void {
:selected-file-ref="selectedFileRef"
:file-loading="taskData.taskLoading.value"
:reindex-loading="source.sourceReindexLoading.value"
:selected-file-name="selectedFileName"
:selected-task-label="selectedTaskLabel"
@update:selected-source-id="selectedSourceId = $event"
@update:selected-file-ref="selectFile($event)"
@open-source-config="openSourceConfig"
@@ -577,25 +562,13 @@ function setError(err: unknown): void {
</template>
<style scoped>
.mdtodo-page { display: grid; height: calc(100dvh - 68px); max-height: calc(100dvh - 68px); min-width: 0; min-height: 0; align-content: stretch; grid-template-rows: auto auto auto minmax(0, 1fr); gap: 0; overflow: hidden; border: 1px solid #cad6dd; border-radius: 8px; background: #f7fafb; }
.mdtodo-framebar { display: flex; min-width: 0; min-height: 34px; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #d8e1e7; background: #f9fbfc; padding: 4px 10px; }
.mdtodo-title-lockup { display: inline-flex; min-width: 0; align-items: baseline; gap: 8px; }
.mdtodo-title-lockup span { color: #6b7a86; font-size: 10px; font-weight: 850; letter-spacing: 0; text-transform: uppercase; }
.mdtodo-title-lockup strong { color: #111827; font-size: 16px; line-height: 1; }
.mdtodo-route-chip { display: inline-flex; min-width: 0; align-items: center; justify-content: flex-end; gap: 8px; color: #52616b; font-size: 12px; font-weight: 750; }
.mdtodo-route-chip span,
.mdtodo-route-chip small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mdtodo-route-chip small { max-width: min(46vw, 720px); color: #0f766e; font: inherit; }
.mdtodo-page { display: grid; height: calc(100dvh - 68px); max-height: calc(100dvh - 68px); min-width: 0; min-height: 0; align-content: stretch; grid-template-rows: auto auto minmax(0, 1fr); gap: 0; overflow: hidden; border: 1px solid #cad6dd; border-radius: 8px; background: #f7fafb; }
.mdtodo-route-loading,
.mdtodo-error { grid-row: 2 / -1; min-width: 0; min-height: 0; height: 100%; align-self: stretch; }
.mdtodo-notices { display: grid; grid-row: 3; min-width: 0; min-height: 0; gap: 0; }
.mdtodo-error { grid-row: 1 / -1; min-width: 0; min-height: 0; height: 100%; align-self: stretch; }
.mdtodo-notices { display: grid; grid-row: 2; min-width: 0; min-height: 0; gap: 0; }
.source-message, .source-error { margin: 0; border-radius: 6px; padding: 6px 10px; font-size: 13px; }
.source-message { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; }
.source-error { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; }
.mdtodo-workspace { grid-row: 4; min-height: 0; height: 100%; max-height: none; align-self: stretch; }
.mdtodo-workspace { grid-row: 3; min-height: 0; height: 100%; max-height: none; align-self: stretch; }
.mdtodo-error { display: grid; align-content: start; gap: 12px; padding: 12px; overflow: auto; }
@media (max-width: 720px) {
.mdtodo-framebar { align-items: start; flex-direction: column; gap: 2px; height: auto; padding: 6px 10px; }
.mdtodo-route-chip { width: 100%; justify-content: flex-start; }
}
</style>
@@ -2,11 +2,12 @@
<!-- Responsibility: main workspace task title, status, body (the primary content), inline edit, report links, Workbench launch. -->
<script setup lang="ts">
import { computed } from "vue";
import type { MdtodoTaskDetailRecord, MdtodoTaskLinkRecord, MdtodoTaskRecord, ProjectNavigationResponse } from "@/api";
import LoadingState from "@/components/common/LoadingState.vue";
import EmptyState from "@/components/common/EmptyState.vue";
defineProps<{
const props = defineProps<{
task: MdtodoTaskRecord | null;
detail: MdtodoTaskDetailRecord | null;
detailLoading: boolean;
@@ -44,6 +45,18 @@ const emit = defineEmits<{
openReport: [link: MdtodoTaskLinkRecord];
launchWorkbench: [];
}>();
const bodyDiffersFromTitle = computed(() => {
const body = normalizeTaskText(props.bodyText);
if (!body) return false;
const title = normalizeTaskText(props.task?.title);
const titleWithId = normalizeTaskText([props.task?.taskId, props.task?.title].filter(Boolean).join(" "));
return body !== title && body !== titleWithId;
});
function normalizeTaskText(value?: string | null): string {
return String(value ?? "").replace(/\s+/g, " ").trim();
}
</script>
<template>
@@ -96,8 +109,8 @@ const emit = defineEmits<{
</div>
</div>
<article v-else class="markdown-body task-body-rendered" data-testid="mdtodo-body-rendered" @dblclick="emit('beginBodyEdit')">
<div v-if="bodyHtml" v-html="bodyHtml" />
<p v-else class="empty-inline">暂无正文</p>
<div v-if="bodyHtml && bodyDiffersFromTitle" v-html="bodyHtml" />
<p v-else class="empty-inline">暂无补充正文</p>
</article>
</section>
</section>
@@ -12,8 +12,6 @@ defineProps<{
selectedFileRef: string | null;
fileLoading: boolean;
reindexLoading: boolean;
selectedFileName: string;
selectedTaskLabel: string;
}>();
const emit = defineEmits<{
@@ -47,10 +45,6 @@ function onFileSelect(event: Event): void {
<option v-for="file in files" :key="file.fileRef" :value="file.fileRef">{{ fileDisplayName(file) }}</option>
</select>
</label>
<div class="toolbar-context" data-testid="mdtodo-toolbar-context">
<span class="toolbar-file-name" :title="selectedFileName">{{ selectedFileName }}</span>
<small v-if="selectedTaskLabel" class="toolbar-task-label">{{ selectedTaskLabel }}</small>
</div>
<div class="toolbar-actions">
<button class="btn btn-secondary toolbar-create" type="button" data-testid="mdtodo-new-task-open" @click="emit('openCreateTask')">+ 任务</button>
<button class="icon-button" type="button" data-testid="mdtodo-source-config-open" aria-label="配置 Source" @click="emit('openSourceConfig')"></button>
@@ -61,20 +55,16 @@ function onFileSelect(event: Event): void {
</template>
<style scoped>
.mdtodo-toolbar { display: grid; grid-template-columns: minmax(160px, 240px) minmax(220px, 360px) minmax(0, 1fr) auto; gap: 8px; align-items: center; border-bottom: 1px solid #d8e1e7; background: #ffffff; padding: 7px 10px; }
.mdtodo-toolbar { display: grid; grid-template-columns: minmax(160px, 240px) minmax(260px, 1fr) auto; gap: 8px; align-items: center; border-bottom: 1px solid #d8e1e7; background: #ffffff; padding: 7px 10px; }
.toolbar-field { display: grid; min-width: 0; grid-template-columns: auto minmax(0, 1fr); gap: 7px; align-items: center; }
.toolbar-field span { color: #5d6a73; font-size: 10px; font-weight: 850; letter-spacing: 0; text-transform: uppercase; }
.toolbar-field select { width: 100%; min-width: 0; height: 32px; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; padding: 0 9px; font: inherit; font-size: 13px; }
.toolbar-context { display: grid; min-width: 0; gap: 1px; align-content: center; border-left: 1px solid #e2e8ed; padding-left: 10px; }
.toolbar-file-name { min-width: 0; overflow: hidden; color: #111827; font-size: 12px; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.toolbar-task-label { color: #0f766e; font-size: 11px; font-weight: 750; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toolbar-actions { display: flex; gap: 6px; justify-content: flex-end; align-items: center; }
.toolbar-create { min-height: 32px; border-color: #a7c2c0; color: #0f4f4a; }
.icon-button { display: inline-grid; width: 32px; height: 32px; place-items: center; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; font-weight: 850; }
.icon-button:disabled { opacity: 0.5; }
@media (max-width: 980px) {
.mdtodo-toolbar { grid-template-columns: 1fr 1fr auto; }
.toolbar-context { display: none; }
}
@media (max-width: 640px) {
.mdtodo-toolbar { grid-template-columns: 1fr; }
+9 -2
View File
@@ -26,7 +26,7 @@ let installed = false;
let flushTimer: number | null = null;
export function installWebRum(): void {
if (installed || typeof window === "undefined") return;
if (installed || !hasBrowserRumRuntime() || typeof window.addEventListener !== "function") return;
installed = true;
window.addEventListener("load", () => window.setTimeout(recordNavigationTiming, 0), { once: true });
window.addEventListener("visibilitychange", () => {
@@ -36,7 +36,7 @@ export function installWebRum(): void {
}
export function recordApiTiming(input: ApiTimingInput): void {
if (typeof window === "undefined") return;
if (!hasBrowserRumRuntime()) return;
if (isWorkbenchPageRoute()) return;
const valueMs = Math.max(0, Date.now() - input.startedAt);
enqueue({
@@ -96,6 +96,7 @@ function enqueue(event: RumEvent): void {
}
function scheduleFlush(): void {
if (!hasBrowserRumRuntime()) return;
if (flushTimer !== null) return;
flushTimer = window.setTimeout(() => flushRum(false), FLUSH_INTERVAL_MS);
}
@@ -116,10 +117,16 @@ function flushRum(useBeacon: boolean): void {
}
function pageRoute(): string {
if (typeof window === "undefined" || !window.location) return "/";
return `${window.location.pathname}${window.location.hash || ""}` || "/";
}
function hasBrowserRumRuntime(): boolean {
return typeof window !== "undefined" && typeof window.setTimeout === "function" && typeof window.clearTimeout === "function";
}
function isWorkbenchPageRoute(): boolean {
if (typeof window === "undefined" || !window.location) return false;
const pathname = window.location.pathname || "";
return pathname.startsWith("/workbench") || pathname.startsWith("/workspace");
}
@@ -506,7 +506,7 @@ export function resetWorkbenchPerformanceForTest(): void {
openJourney = null;
queue = [];
uiTrace = null;
if (flushTimer !== null && typeof window !== "undefined") window.clearTimeout(flushTimer);
if (flushTimer !== null && hasBrowserTimerRuntime()) window.clearTimeout(flushTimer);
flushTimer = null;
installed = false;
}
@@ -542,12 +542,12 @@ function enqueue(event: WorkbenchPerformanceEvent): void {
}
function scheduleFlush(): void {
if (typeof window === "undefined" || flushTimer !== null) return;
if (!hasBrowserTimerRuntime() || flushTimer !== null) return;
flushTimer = window.setTimeout(() => flushWorkbenchPerformance(false), FLUSH_INTERVAL_MS);
}
function flushWorkbenchPerformance(useBeacon: boolean): void {
if (flushTimer !== null && typeof window !== "undefined") {
if (flushTimer !== null && hasBrowserTimerRuntime()) {
window.clearTimeout(flushTimer);
flushTimer = null;
}
@@ -686,10 +686,14 @@ function timestampMs(value: unknown): number | null {
}
function pageRoute(): string {
if (typeof window === "undefined") return "/workbench";
if (typeof window === "undefined" || !window.location) return "/workbench";
return routeTemplate(`${window.location.pathname}${window.location.hash || ""}` || "/workbench");
}
function hasBrowserTimerRuntime(): boolean {
return typeof window !== "undefined" && typeof window.setTimeout === "function" && typeof window.clearTimeout === "function";
}
function routeTemplate(input: string): string {
const raw = safeText(input) || "/workbench";
const pathOnly = raw.startsWith("#") ? raw.slice(1) || "/" : raw.split(/[?#]/u, 1)[0] || "/";