Merge pull request #2370 from pikasTech/fix/2369-mdtodo-layout
fix: MDTODO 前端布局 — 10 项布局修复
This commit is contained in:
@@ -38,6 +38,8 @@ const props = withDefaults(defineProps<{
|
||||
const emit = defineEmits<{
|
||||
"update:leftWidth": [value: number];
|
||||
"update:rightWidth": [value: number];
|
||||
"update:leftCollapsed": [value: boolean];
|
||||
"update:rightCollapsed": [value: boolean];
|
||||
}>();
|
||||
|
||||
const layoutRef = ref<HTMLElement | null>(null);
|
||||
@@ -51,11 +53,13 @@ const layoutStyle = computed(() => {
|
||||
? `${props.leftCollapsedWidth}px`
|
||||
: `minmax(${props.leftCollapsedWidth}px, ${clampedLeftWidth.value}%)`;
|
||||
if (!props.rightOpen) {
|
||||
return { gridTemplateColumns: `${leftColumn} 6px minmax(0, 1fr)` };
|
||||
const leftCol = props.leftCollapsed ? '0px' : leftColumn;
|
||||
const resizerCol = props.leftCollapsed ? '0px' : '6px';
|
||||
return { gridTemplateColumns: `${leftCol} ${resizerCol} minmax(0, 1fr)` };
|
||||
}
|
||||
const mainShare = props.rightCollapsed ? "minmax(0, 1fr)" : `minmax(0, ${100 - clampedRightWidth.value}fr)`;
|
||||
const rightResizeColumn = props.rightCollapsed ? "0" : "6px";
|
||||
const rightColumn = props.rightCollapsed ? `${props.rightCollapsedWidth}px` : `minmax(280px, ${clampedRightWidth.value}fr)`;
|
||||
const rightColumn = props.rightCollapsed ? "0px" : `minmax(280px, ${clampedRightWidth.value}fr)`;
|
||||
return { gridTemplateColumns: `${leftColumn} 6px ${mainShare} ${rightResizeColumn} ${rightColumn}` };
|
||||
});
|
||||
|
||||
@@ -105,9 +109,12 @@ function clamp(value: number, min: number, max: number): number {
|
||||
:data-right-collapsed="rightCollapsed"
|
||||
:data-resizing="resizing || undefined"
|
||||
>
|
||||
<aside class="split-workspace-pane split-workspace-left" :data-collapsed="leftCollapsed">
|
||||
<aside v-if="!leftCollapsed" class="split-workspace-pane split-workspace-left" :data-collapsed="leftCollapsed">
|
||||
<slot name="left" />
|
||||
</aside>
|
||||
<button v-if="leftCollapsed" class="split-workspace-toggle split-workspace-toggle-left" type="button" aria-label="展开左侧面板" @click="$emit('update:leftCollapsed', false)">
|
||||
<slot name="left-toggle">›</slot>
|
||||
</button>
|
||||
<div
|
||||
class="split-workspace-resizer split-workspace-resizer-left"
|
||||
:data-testid="leftResizerTestId"
|
||||
@@ -128,9 +135,12 @@ function clamp(value: number, min: number, max: number): number {
|
||||
:aria-valuenow="Math.round(clampedRightWidth)"
|
||||
@pointerdown="startResize('right', $event)"
|
||||
/>
|
||||
<aside v-if="rightOpen" class="split-workspace-pane split-workspace-right" :data-collapsed="rightCollapsed">
|
||||
<aside v-if="rightOpen && !rightCollapsed" class="split-workspace-pane split-workspace-right" :data-collapsed="rightCollapsed">
|
||||
<slot name="right" />
|
||||
</aside>
|
||||
<button v-if="rightOpen && rightCollapsed" class="split-workspace-toggle split-workspace-toggle-right" type="button" aria-label="展开右侧面板" @click="$emit('update:rightCollapsed', false)">
|
||||
<slot name="right-toggle">‹</slot>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -179,8 +189,7 @@ function clamp(value: number, min: number, max: number): number {
|
||||
|
||||
.split-workspace-layout[data-left-collapsed="true"] .split-workspace-resizer-left,
|
||||
.split-workspace-layout[data-right-collapsed="true"] .split-workspace-resizer-right {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.split-workspace-layout[data-resizing] {
|
||||
@@ -203,6 +212,11 @@ function clamp(value: number, min: number, max: number): number {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.split-workspace-toggle { display: inline-flex; width: 28px; height: 100%; align-items: center; justify-content: center; border: 1px solid #d8e1e7; border-radius: 0; background: #f8fafb; color: #5d6a73; cursor: pointer; font-size: 16px; font-weight: 850; }
|
||||
.split-workspace-toggle:hover { background: #eef2f5; color: #111827; }
|
||||
.split-workspace-toggle-left { border-right: 0; }
|
||||
.split-workspace-toggle-right { border-left: 0; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.split-workspace-layout {
|
||||
display: grid;
|
||||
|
||||
@@ -477,6 +477,10 @@ async function reindexSource(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function dismissSourceMessage(): void {
|
||||
source.clearSourceMessages();
|
||||
}
|
||||
|
||||
function setError(err: unknown): void {
|
||||
if (err && typeof err === "object" && "error" in err) {
|
||||
const result = err as { error?: string | null; apiError?: ApiError | null; diagnostic?: ErrorDiagnostic | null; status?: number };
|
||||
@@ -515,7 +519,10 @@ function setError(err: unknown): void {
|
||||
/>
|
||||
|
||||
<div v-if="source.sourceMessage.value || source.sourceError.value" class="mdtodo-notices">
|
||||
<p v-if="source.sourceMessage.value" class="source-message" data-testid="mdtodo-source-message">{{ source.sourceMessage.value }}</p>
|
||||
<div v-if="source.sourceMessage.value" class="source-message" data-testid="mdtodo-source-message">
|
||||
<span>{{ source.sourceMessage.value }}</span>
|
||||
<button class="source-message-close" type="button" @click="dismissSourceMessage" aria-label="关闭提示">×</button>
|
||||
</div>
|
||||
<p v-if="source.sourceError.value" class="source-error" data-testid="mdtodo-source-error">{{ source.sourceError.value }}</p>
|
||||
</div>
|
||||
|
||||
@@ -671,6 +678,10 @@ function setError(err: unknown): void {
|
||||
.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 { display: flex; align-items: center; gap: 8px; }
|
||||
.source-message span { flex: 1; }
|
||||
.source-message-close { display: inline-flex; width: 22px; height: 22px; align-items: center; justify-content: center; border: 1px solid #bbf7d0; border-radius: 4px; background: transparent; color: #166534; cursor: pointer; font-size: 14px; font-weight: 750; line-height: 1; flex-shrink: 0; }
|
||||
.source-message-close:hover { background: #dcfce7; }
|
||||
.source-message { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; }
|
||||
.source-error { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; }
|
||||
.mdtodo-workspace { grid-row: 3; min-height: 0; height: 100%; max-height: none; align-self: stretch; }
|
||||
|
||||
@@ -51,13 +51,13 @@ const emit = defineEmits<{
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.report-sidebar { display: grid; min-width: 0; min-height: 0; height: auto; max-height: 100%; align-content: start; grid-template-rows: auto auto; background: #f8fafb; overflow: hidden; }
|
||||
.report-sidebar { display: grid; min-width: 0; min-height: 0; height: 100%; max-height: none; align-content: start; grid-template-rows: auto minmax(0, 1fr); background: #f8fafb; overflow: hidden; }
|
||||
.report-sidebar[data-collapsed="true"] { justify-items: center; padding: 8px 4px; grid-template-rows: auto; }
|
||||
.report-sidebar-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; border-bottom: 1px solid #d8e1e7; color: #111827; font-size: 13px; padding: 9px 10px; }
|
||||
.report-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.report-sidebar-actions { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; }
|
||||
.report-preview { display: grid; min-height: 0; gap: 8px; overflow: hidden; padding: 10px; }
|
||||
.report-preview > .markdown-body { min-height: 0; max-height: min(52dvh, 580px); overflow: auto; }
|
||||
.report-preview > .markdown-body { min-height: 0; height: 100%; max-height: none; overflow: auto; }
|
||||
.report-loading { color: #64706b; font-size: 13px; padding: 12px; }
|
||||
.markdown-body { min-width: 0; color: #111827; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
|
||||
.markdown-body :deep(p), .markdown-body :deep(ul), .markdown-body :deep(ol), .markdown-body :deep(pre), .markdown-body :deep(blockquote) { margin: 0 0 8px; }
|
||||
|
||||
@@ -125,7 +125,8 @@ function normalizeTaskText(value?: string | null): string {
|
||||
</div>
|
||||
<article v-else class="markdown-body task-body-rendered" data-testid="mdtodo-body-rendered" @dblclick="emit('beginBodyEdit')">
|
||||
<div v-if="bodyHtml && bodyDiffersFromTitle" v-html="bodyHtml" />
|
||||
<p v-else class="empty-inline">暂无补充正文</p>
|
||||
<p v-else-if="!task" class="empty-inline">暂无补充正文</p>
|
||||
<p v-else class="empty-inline">{{ task.title || task.taskId || '暂无正文' }}</p>
|
||||
</article>
|
||||
</section>
|
||||
</RefreshBoundary>
|
||||
@@ -153,14 +154,14 @@ function normalizeTaskText(value?: string | null): string {
|
||||
|
||||
<style scoped>
|
||||
.mdtodo-detail-panel { display: grid; min-width: 0; min-height: 0; height: 100%; max-height: none; align-content: stretch; grid-template-rows: auto auto minmax(0, 1fr) auto; gap: 0; overflow: hidden; background: #fff; }
|
||||
.mdtodo-detail-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 10px; border-bottom: 1px solid #e2e8ed; padding: 10px 12px; }
|
||||
.mdtodo-detail-header { display: grid; min-width: 0; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; gap: 8px; border-bottom: 1px solid #e2e8ed; padding: 10px 12px; }
|
||||
.task-title-block { display: grid; min-width: 0; grid-template-columns: auto minmax(0, 1fr); gap: 8px; align-items: center; }
|
||||
.task-id-large { display: inline-flex; min-width: 46px; justify-content: center; border: 1px solid #9be3dc; border-radius: 6px; background: #e8faf6; color: #0f766e; padding: 5px 8px; font-size: 12px; font-weight: 850; }
|
||||
.task-title-read { min-width: 0; overflow: hidden; border: 0; border-radius: 5px; background: transparent; color: #111827; padding: 5px 6px; font: inherit; font-size: 18px; font-weight: 850; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.task-title-read:hover { background: #f3f8f8; }
|
||||
.inline-editor { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; align-items: center; }
|
||||
.inline-editor input { width: 100%; min-width: 0; border: 1px solid #cbd6dd; border-radius: 6px; color: #111827; font: inherit; padding: 8px 9px; }
|
||||
.detail-toolbar { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
.detail-toolbar { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 8px; grid-row: 2; }
|
||||
.detail-toolbar select { min-width: 108px; height: 32px; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; padding: 0 8px; font: inherit; font-size: 13px; }
|
||||
.provider-field { display: flex; min-width: 218px; align-items: center; gap: 6px; color: #5d6a73; font-size: 12px; font-weight: 850; }
|
||||
.provider-field span { white-space: nowrap; }
|
||||
|
||||
@@ -82,12 +82,12 @@ function toggleTask(task: MdtodoTaskRecord): void {
|
||||
</template>
|
||||
<div class="task-tree" role="tree" aria-label="MDTODO task tree" :aria-busy="loading ? 'true' : 'false'" :data-refreshing="loading ? 'true' : 'false'">
|
||||
<div v-for="task in visibleTaskRows" :key="task.taskRef" class="task-row-shell" :style="indentStyle(task)">
|
||||
<button class="task-toggle" type="button" :data-testid="tree.hasChildren(task) ? 'mdtodo-task-toggle' : undefined" :aria-label="props.collapsedTaskRefs.has(task.taskRef) ? '展开任务' : '折叠任务'" :disabled="!tree.hasChildren(task)" @click.stop="toggleTask(task)">{{ tree.hasChildren(task) ? (props.collapsedTaskRefs.has(task.taskRef) ? '+' : '-') : '' }}</button>
|
||||
<button class="task-row" role="treeitem" type="button" :data-selected="task.taskRef === selectedTaskRef" :data-task-ref="task.taskRef" :data-task-id="task.taskId" :data-task-status="task.status || 'unknown'" @click="emit('selectTask', task.taskRef)">
|
||||
<span class="task-id">{{ task.taskId || 'R?' }}</span>
|
||||
<strong>{{ task.title || task.taskId || 'Untitled task' }}</strong>
|
||||
<span class="task-status" :data-status="task.status || 'unknown'">{{ statusLabel(task.status) }}</span>
|
||||
</button>
|
||||
<button class="task-toggle" type="button" :data-testid="tree.hasChildren(task) ? 'mdtodo-task-toggle' : undefined" :aria-label="props.collapsedTaskRefs.has(task.taskRef) ? '展开任务' : '折叠任务'" :disabled="!tree.hasChildren(task)" @click.stop="toggleTask(task)">{{ tree.hasChildren(task) ? (props.collapsedTaskRefs.has(task.taskRef) ? '+' : '-') : '' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</RefreshBoundary>
|
||||
@@ -96,7 +96,7 @@ function toggleTask(task: MdtodoTaskRecord): void {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mdtodo-task-panel { display: grid; min-width: 0; min-height: 0; height: auto; max-height: 100%; align-content: start; grid-template-rows: auto auto auto; gap: 8px; overflow: hidden; padding: 10px 8px 10px 10px; }
|
||||
.mdtodo-task-panel { display: grid; min-width: 0; min-height: 0; height: 100%; max-height: none; align-content: start; grid-template-rows: auto auto minmax(0, 1fr); gap: 8px; overflow: hidden; padding: 10px 8px 10px 10px; }
|
||||
.mdtodo-task-panel[data-collapsed="true"] { justify-items: center; padding: 8px 4px; overflow: hidden; grid-template-rows: auto; }
|
||||
.mdtodo-panel-header { display: grid; min-width: 0; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; }
|
||||
.task-tree-title { min-width: 0; }
|
||||
@@ -110,19 +110,21 @@ function toggleTask(task: MdtodoTaskRecord): void {
|
||||
.task-tree-boundary { display: grid; min-height: 0; }
|
||||
.task-tree { display: grid; min-height: 0; max-height: min(54dvh, 560px); align-content: start; gap: 1px; overflow: auto; border-top: 1px solid #e3e9ee; padding-top: 2px; }
|
||||
.task-tree[data-refreshing="true"] { border-top-color: #b7e4df; }
|
||||
.task-row-shell { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 4px; align-items: center; }
|
||||
.task-row-shell { display: grid; grid-template-columns: minmax(0, 1fr) 24px; gap: 4px; align-items: center; }
|
||||
.task-toggle { width: 24px; height: 24px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: #52616b; font-weight: 850; line-height: 1; }
|
||||
.task-toggle:hover:not(:disabled) { border-color: #cbd6dd; background: #fff; }
|
||||
.task-toggle:disabled { border-color: transparent; background: transparent; }
|
||||
.task-row { display: grid; width: 100%; min-width: 0; grid-template-columns: minmax(44px, 0.2fr) minmax(0, 1fr) auto; gap: 7px; align-items: center; border: 1px solid transparent; border-radius: 5px; background: transparent; color: #111827; padding: 7px 8px; text-align: left; }
|
||||
.task-row { display: grid; width: 100%; min-width: 0; grid-template-columns: auto minmax(0, 1fr) auto; gap: 7px; align-items: center; border: 1px solid transparent; border-radius: 5px; background: transparent; color: #111827; padding: 7px 8px; text-align: left; }
|
||||
.task-row:hover { border-color: #d9e2e8; background: #fff; }
|
||||
.task-row[data-selected="true"] { border-color: #5bb7af; background: #e8faf6; box-shadow: inset 3px 0 0 #0f766e; }
|
||||
.task-id { color: #0f766e; font-size: 12px; font-weight: 850; }
|
||||
.task-id { color: #0f766e; font-size: 12px; font-weight: 850; white-space: nowrap; }
|
||||
.task-row strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.task-status { display: inline-flex; width: fit-content; max-width: 100%; align-items: center; border-radius: 999px; border: 1px solid #bae6fd; background: #f0f9ff; color: #075985; padding: 3px 7px; font-size: 10px; font-weight: 750; line-height: 1; }
|
||||
.task-status[data-status="done"] { border-color: #86efac; background: #dcfce7; color: #166534; }
|
||||
.task-status[data-status="in_progress"] { border-color: #fde68a; background: #fffbeb; color: #92400e; }
|
||||
.task-status[data-status="blocked"] { border-color: #fed7aa; background: #fff7ed; color: #9a3412; }
|
||||
.task-status { display: inline; width: auto; max-width: none; border: 0; border-radius: 0; background: transparent; color: #075985; padding: 0; font-size: 10px; font-weight: 850; line-height: inherit; }
|
||||
.task-status::before { content: "["; }
|
||||
.task-status::after { content: "]"; }
|
||||
.task-status[data-status="done"] { color: #166534; }
|
||||
.task-status[data-status="in_progress"] { color: #92400e; }
|
||||
.task-status[data-status="blocked"] { color: #9a3412; }
|
||||
.icon-button { display: inline-grid; width: 30px; height: 30px; place-items: center; border: 1px solid #cbd6dd; border-radius: 6px; background: #fff; color: #111827; font-weight: 850; }
|
||||
.icon-button:disabled { opacity: 0.5; }
|
||||
@keyframes task-refresh-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
@@ -200,7 +200,7 @@ export function useMdtodoWorkbenchLaunch(router: Router) {
|
||||
}
|
||||
|
||||
async function navigateToWorkbench(result: { sessionId: string; workbenchUrl: string }): Promise<void> {
|
||||
await router.push(result.workbenchUrl);
|
||||
window.open(result.workbenchUrl, '_blank');
|
||||
}
|
||||
|
||||
function isLaunchEnabled(navigation: ProjectNavigationResponse | null): boolean {
|
||||
|
||||
Reference in New Issue
Block a user