Skip to content

Commit d186bac

Browse files
authored
fix: Unable to load frontend page after refreshing (#4930)
1 parent 968259a commit d186bac

File tree

6 files changed

+195
-64
lines changed

6 files changed

+195
-64
lines changed

ui/src/App.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
<script setup lang="ts">
2-
import PopoverManager from '@/components/popover-manager/index.vue'
3-
</script>
4-
1+
<script setup lang="ts"></script>
52
<template>
63
<RouterView />
7-
<PopoverManager></PopoverManager>
84
</template>

ui/src/chat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import router from '@/router/chat'
1111
import i18n from '@/locales'
1212
import Components from '@/components'
1313
import directives from '@/directives'
14-
14+
import { supPopover } from '@/utils/supPopover'
1515
import { getDefaultWhiteList } from 'xss'
1616
import { config, XSSPlugin } from 'md-editor-v3'
1717
import screenfull from 'screenfull'
@@ -90,6 +90,7 @@ config({
9090
]
9191
},
9292
})
93+
supPopover.init()
9394
const app = createApp(App)
9495
app.use(createPinia())
9596
for (const [key, component] of Object.entries(ElementPlusIcons)) {

ui/src/components/popover-manager/index.vue

Lines changed: 0 additions & 58 deletions
This file was deleted.

ui/src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import directives from '@/directives'
1414
import { getDefaultWhiteList } from 'xss'
1515
import { config, XSSPlugin } from 'md-editor-v3'
1616
import screenfull from 'screenfull'
17+
import { supPopover } from '@/utils/supPopover'
1718

1819
import katex from 'katex'
1920
import 'katex/dist/katex.min.css'
@@ -90,6 +91,7 @@ config({
9091
]
9192
},
9293
})
94+
supPopover.init()
9395
const app = createApp(App)
9496
app.use(createPinia())
9597
for (const [key, component] of Object.entries(ElementPlusIcons)) {

ui/src/styles/app.scss

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,28 @@ h5 {
565565
.arrow-icon {
566566
transition: 0.2s;
567567
}
568+
.sup-popover {
569+
display: none;
570+
position: absolute;
571+
top: 0;
572+
left: 0;
573+
z-index: 9999;
574+
width: 240px;
575+
padding: 10px 12px;
576+
border-radius: 4px;
577+
background: #fff;
578+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
579+
font-size: 13px;
580+
line-height: 1.5;
581+
color: #303133;
582+
word-break: break-word;
583+
}
584+
585+
.sup-popover__arrow {
586+
position: absolute;
587+
width: 8px;
588+
height: 8px;
589+
background: #fff;
590+
transform: rotate(45deg);
591+
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.08);
592+
}

ui/src/utils/supPopover.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// supPopover.ts
2+
import {
3+
computePosition,
4+
flip,
5+
offset,
6+
shift,
7+
arrow,
8+
autoUpdate,
9+
type Placement,
10+
} from '@floating-ui/dom'
11+
import DOMPurify from 'dompurify'
12+
let tooltipEl: HTMLDivElement | null = null
13+
let arrowEl: HTMLDivElement | null = null
14+
let contentEl: HTMLDivElement | null = null
15+
let currentSup: HTMLElement | null = null
16+
let cleanupAutoUpdate: (() => void) | null = null
17+
let initialized = false
18+
19+
// ---- 创建 tooltip DOM ----
20+
function createTooltip() {
21+
const el = document.createElement('div')
22+
el.className = 'sup-popover'
23+
24+
const inner = document.createElement('div')
25+
inner.className = 'sup-popover__content'
26+
el.appendChild(inner)
27+
28+
const av = document.createElement('div')
29+
av.className = 'sup-popover__arrow'
30+
el.appendChild(av)
31+
32+
document.body.appendChild(el)
33+
return { el, arrowEl: av, contentEl: inner }
34+
}
35+
36+
// ---- 定位计算 ----
37+
async function updatePosition(reference: HTMLElement) {
38+
if (!tooltipEl || !arrowEl) return
39+
40+
const { x, y, placement, middlewareData } = await computePosition(reference, tooltipEl, {
41+
placement: 'top',
42+
middleware: [offset(10), flip(), shift({ padding: 8 }), arrow({ element: arrowEl })],
43+
})
44+
45+
Object.assign(tooltipEl.style, { left: `${x}px`, top: `${y}px` })
46+
tooltipEl.dataset.placement = placement
47+
48+
const { x: ax, y: ay } = middlewareData.arrow ?? {}
49+
const side = placement.split('-')[0] as 'top' | 'bottom' | 'left' | 'right'
50+
const staticSide = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]
51+
52+
Object.assign(arrowEl.style, {
53+
left: '',
54+
top: '',
55+
right: '',
56+
bottom: '',
57+
...(ax != null ? { left: `${ax}px` } : {}),
58+
...(ay != null ? { top: `${ay}px` } : {}),
59+
[staticSide]: '-5px',
60+
})
61+
}
62+
63+
// ---- 显示 / 隐藏 ----
64+
function show(sup: HTMLElement) {
65+
if (!tooltipEl || !contentEl) return
66+
67+
// 过滤 XSS,只保留安全的 HTML 标签和属性
68+
contentEl.innerHTML = DOMPurify.sanitize(sup.dataset.title ?? '')
69+
70+
tooltipEl.style.display = 'block'
71+
tooltipEl.style.pointerEvents = 'auto'
72+
73+
cleanupAutoUpdate?.()
74+
cleanupAutoUpdate = autoUpdate(sup, tooltipEl, () => updatePosition(sup))
75+
}
76+
77+
function hide() {
78+
if (!tooltipEl) return
79+
tooltipEl.style.display = 'none'
80+
cleanupAutoUpdate?.()
81+
cleanupAutoUpdate = null
82+
currentSup = null
83+
}
84+
85+
// ---- 核心:用 pointer 路径判断是否在安全区内 ----
86+
// 记录鼠标坐标
87+
let mouseX = 0
88+
let mouseY = 0
89+
document.addEventListener(
90+
'mousemove',
91+
(e) => {
92+
mouseX = e.clientX
93+
mouseY = e.clientY
94+
},
95+
{ passive: true },
96+
)
97+
98+
function isMouseInsideSafeZone(): boolean {
99+
if (!tooltipEl || !currentSup) return false
100+
101+
const supRect = currentSup.getBoundingClientRect()
102+
const tipRect = tooltipEl.getBoundingClientRect()
103+
104+
// 把 sup 和 tooltip 的 rect 各扩展 2px 容差,
105+
// 再判断鼠标是否在两个矩形的凸包(union bbox)内
106+
const pad = 2
107+
const minX = Math.min(supRect.left, tipRect.left) - pad
108+
const maxX = Math.max(supRect.right, tipRect.right) + pad
109+
const minY = Math.min(supRect.top, tipRect.top) - pad
110+
const maxY = Math.max(supRect.bottom, tipRect.bottom) + pad
111+
112+
return mouseX >= minX && mouseX <= maxX && mouseY >= minY && mouseY <= maxY
113+
}
114+
115+
// ---- 事件处理 ----
116+
function onMouseOver(e: MouseEvent) {
117+
const sup = (e.target as HTMLElement).closest('sup[data-title]') as HTMLElement | null
118+
if (!sup) return
119+
120+
if (sup !== currentSup) {
121+
currentSup = sup
122+
show(sup)
123+
}
124+
}
125+
126+
function onMouseMove(e: MouseEvent) {
127+
if (!currentSup) return
128+
129+
// 鼠标在 tooltip 内部,直接跳过,不做任何处理
130+
if (tooltipEl && (e.target === tooltipEl || tooltipEl.contains(e.target as Node))) return
131+
132+
const overSup = (e.target as HTMLElement).closest('sup[data-title]')
133+
134+
if (!overSup && !isMouseInsideSafeZone()) {
135+
hide()
136+
}
137+
}
138+
139+
// ---- 单例公共 API ----
140+
export const supPopover = {
141+
init() {
142+
if (initialized) return
143+
initialized = true
144+
145+
const els = createTooltip()
146+
tooltipEl = els.el
147+
arrowEl = els.arrowEl
148+
contentEl = els.contentEl
149+
150+
document.addEventListener('mouseover', onMouseOver)
151+
document.addEventListener('mousemove', onMouseMove, { passive: true })
152+
},
153+
154+
destroy() {
155+
document.removeEventListener('mouseover', onMouseOver)
156+
document.removeEventListener('mousemove', onMouseMove)
157+
cleanupAutoUpdate?.()
158+
tooltipEl?.remove()
159+
tooltipEl = null
160+
arrowEl = null
161+
contentEl = null
162+
currentSup = null
163+
initialized = false
164+
},
165+
}

0 commit comments

Comments
 (0)