feat(chat): 编辑摘要卡片弹窗改为锚定定位并修复深色近黑样式
- 弹窗水平相对编辑卡片居中,垂直优先锚定条目上方(空间不足自动翻到下方),高度吃满可用空间 - 行 hover 改用 --hover-bg(深色提亮),弹窗头部去近黑衬底改用分隔线 - 行高亮 is-active 仅钉住态挂载,修复 hover 消失延迟
This commit is contained in:
parent
56a0acdad2
commit
79f8ffa368
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user