feat(chat): 编辑摘要卡片弹窗改为锚定定位并修复深色近黑样式

- 弹窗水平相对编辑卡片居中,垂直优先锚定条目上方(空间不足自动翻到下方),高度吃满可用空间
- 行 hover 改用 --hover-bg(深色提亮),弹窗头部去近黑衬底改用分隔线
- 行高亮 is-active 仅钉住态挂载,修复 hover 消失延迟
This commit is contained in:
JOJO 2026-08-11 23:38:47 +08:00
parent 56a0acdad2
commit 79f8ffa368

View File

@ -49,6 +49,11 @@ const totals = computed(() => {
// ---------------- ----------------
const activePath = ref<string | null>(null);
const pinned = ref(false);
const cardRef = ref<HTMLElement | null>(null);
const anchorEl = ref<HTMLElement | null>(null);
//
// 退 CSS
const panelStyle = ref<Record<string, string>>({});
let openTimer: ReturnType<typeof setTimeout> | null = null;
let closeTimer: ReturnType<typeof setTimeout> | null = null;
@ -57,6 +62,76 @@ const activeFile = computed<SummaryFile | null>(() => {
return files.value.find((f) => f.path === activePath.value) || null;
});
// ---------------- ----------------
const PANEL_MAX_WIDTH = 720;
const VIEWPORT_MARGIN = 16;
const MIN_PANEL_HEIGHT = 140;
//
const PREFER_ABOVE_MIN_HEIGHT = 260;
function computePanelStyle() {
if (isMobile.value || !cardRef.value || !anchorEl.value) {
// / 退 CSS
panelStyle.value = {};
return;
}
const vw = window.innerWidth;
const vh = window.innerHeight;
const width = Math.min(PANEL_MAX_WIDTH, vw - VIEWPORT_MARGIN * 2);
//
const cardRect = cardRef.value.getBoundingClientRect();
const centerX = cardRect.left + cardRect.width / 2;
const left = Math.min(
Math.max(centerX - width / 2, VIEWPORT_MARGIN),
vw - width - VIEWPORT_MARGIN
);
// = -
// = +
//
const rowRect = anchorEl.value.getBoundingClientRect();
const anchorAbove = rowRect.top - rowRect.height / 2;
const anchorBelow = rowRect.bottom + rowRect.height / 2;
const spaceAbove = anchorAbove - VIEWPORT_MARGIN;
const spaceBelow = vh - VIEWPORT_MARGIN - anchorBelow;
const placeAbove = spaceAbove >= PREFER_ABOVE_MIN_HEIGHT || spaceAbove >= spaceBelow;
const base: Record<string, string> = {
left: `${Math.round(left)}px`,
width: `${Math.round(width)}px`,
transform: 'none'
};
if (placeAbove) {
const bottomEdge = Math.min(
Math.max(anchorAbove, VIEWPORT_MARGIN + MIN_PANEL_HEIGHT),
vh - VIEWPORT_MARGIN
);
panelStyle.value = {
...base,
top: 'auto',
bottom: `${Math.round(vh - bottomEdge)}px`,
maxHeight: `${Math.round(bottomEdge - VIEWPORT_MARGIN)}px`
};
} else {
const topEdge = Math.min(
Math.max(anchorBelow, VIEWPORT_MARGIN),
vh - VIEWPORT_MARGIN - MIN_PANEL_HEIGHT
);
panelStyle.value = {
...base,
top: `${Math.round(topEdge)}px`,
bottom: 'auto',
maxHeight: `${Math.round(vh - VIEWPORT_MARGIN - topEdge)}px`
};
}
}
function onWindowReposition() {
if (!activePath.value) return;
computePanelStyle();
}
function clearOpenTimer() {
if (openTimer) {
clearTimeout(openTimer);
@ -71,16 +146,18 @@ function clearCloseTimer() {
}
}
function open(path: string, pin: boolean) {
function open(path: string, pin: boolean, rowEl: HTMLElement | null = null) {
clearOpenTimer();
clearCloseTimer();
activePath.value = path;
pinned.value = pin;
anchorEl.value = rowEl;
computePanelStyle();
}
function scheduleOpen(path: string) {
function scheduleOpen(path: string, rowEl: HTMLElement | null) {
clearOpenTimer();
openTimer = setTimeout(() => open(path, false), 150);
openTimer = setTimeout(() => open(path, false, rowEl), 150);
}
function scheduleClose() {
@ -91,10 +168,10 @@ function scheduleClose() {
}, 200);
}
function onRowMouseEnter(path: string) {
function onRowMouseEnter(path: string, e: MouseEvent) {
if (isMobile.value) return;
clearCloseTimer();
scheduleOpen(path);
scheduleOpen(path, (e.currentTarget as HTMLElement) || null);
}
function onRowMouseLeave() {
@ -111,9 +188,9 @@ function onPanelMouseLeave() {
if (!isMobile.value) scheduleClose();
}
function onRowClick(path: string) {
function onRowClick(path: string, e: MouseEvent) {
// = hover
open(path, true);
open(path, true, (e.currentTarget as HTMLElement) || null);
}
function close() {
@ -121,6 +198,7 @@ function close() {
clearCloseTimer();
pinned.value = false;
activePath.value = null;
anchorEl.value = null;
}
function onKeydown(e: KeyboardEvent) {
@ -132,8 +210,13 @@ function onKeydown(e: KeyboardEvent) {
watch(activePath, (v) => {
if (v) {
window.addEventListener('keydown', onKeydown);
window.addEventListener('resize', onWindowReposition);
//
window.addEventListener('scroll', onWindowReposition, true);
} else {
window.removeEventListener('keydown', onKeydown);
window.removeEventListener('resize', onWindowReposition);
window.removeEventListener('scroll', onWindowReposition, true);
}
});
@ -141,6 +224,8 @@ onBeforeUnmount(() => {
clearOpenTimer();
clearCloseTimer();
window.removeEventListener('keydown', onKeydown);
window.removeEventListener('resize', onWindowReposition);
window.removeEventListener('scroll', onWindowReposition, true);
});
// ---------------- diff ----------------
@ -163,7 +248,7 @@ function lineNumber(line: any): string {
</script>
<template>
<div v-if="files.length" class="edit-summary-card">
<div v-if="files.length" class="edit-summary-card" ref="cardRef">
<div class="edit-summary-card__header">
<span
class="icon icon-sm edit-summary-card__icon"
@ -181,10 +266,10 @@ function lineNumber(line: any): string {
v-for="file in files"
:key="file.path"
class="edit-summary-card__row"
:class="{ 'is-active': activePath === file.path }"
@mouseenter="onRowMouseEnter(file.path)"
:class="{ 'is-active': pinned && activePath === file.path }"
@mouseenter="onRowMouseEnter(file.path, $event)"
@mouseleave="onRowMouseLeave"
@click.stop="onRowClick(file.path)"
@click.stop="onRowClick(file.path, $event)"
>
<span class="edit-summary-card__path" :title="file.path">{{ file.path }}</span>
<span class="edit-summary-card__delta">
@ -205,6 +290,7 @@ function lineNumber(line: any): string {
></div>
<div
class="es-modal__panel"
:style="panelStyle"
role="dialog"
:aria-label="`文件变更:${activeFile.path}`"
@mouseenter="onPanelMouseEnter"
@ -313,9 +399,12 @@ function lineNumber(line: any): string {
font-size: 12.5px;
}
/* is-active hover
否则鼠标离开后会被 200ms 关闭计时器拖住看起来像 hover 消失有延迟 */
.edit-summary-card__row:hover,
.edit-summary-card__row.is-active {
background: var(--surface-soft);
/* 用全局 hover token深色下是白色微量 tint提亮避免 surface-soft 的近黑衬底 */
background: var(--hover-bg);
}
.edit-summary-card__path {
@ -374,8 +463,8 @@ function lineNumber(line: any): string {
height: 44px;
padding: 0 12px;
flex: none;
/* 与版本管理弹窗一致:头部不加衬底,靠分隔线区分(深色下避免近黑色块) */
border-bottom: 1px solid var(--border-default);
background: var(--surface-soft);
}
.es-modal__header-icon {