小站修补琐记2

上一个修补琐记太古早了,再新建一个占个坑

2026/2/28: 真の缓存 & 一些小修小补

终于有时间倒腾一下小站了,把友链抽奖机的图片改成真·缓存,并且上传了近几次比赛写了的WP

原来的缓存是D老师写的,原理是预先访问一次,然后浏览器进行“缓存”

后来想起来为什么不用blob呢,于是就写了一个真·缓存,大致如下

1
2
3
4
const response = await fetch(friend.avatar);  // 使用fetch获得资源
const blob = await response.blob(); // 将资源读取成Blob对象
const cachedURL = URL.createObjectURL(blob); // 生成blob:链接
friend.avatar = cachedURL; // 将图像链接用blob:链接替换,使用时浏览器直接从内存读取

同时更新/修正了关于页的内容和样式,在友链抽奖机中加入了求友链的说明

2026/4/13: 友链++ & 小修小补 & 一个决定?

今天加了不少友链,开心~

对图片缓存逻辑进行了一点小修,try catch了一下url不支持CORS的情况,退回到浏览器自带缓存机制

以及决定择期上线一个每咕一题页面,闲的没事可以搓几道史给大家尝尝

2026/4/17: 每咕一题调试完成上线

经过几天高强度的手搓代码,终于,每咕一题完成上线了!

alt text

本次代码保证纯手工打造,100%原汁原味(bushi

好了,其实还是让D老师帮忙写了一下HistoryAPI有关的代码的,这个东西我实在是不想吐槽了

下面展示一些技术细节

Tag组开关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
document.querySelectorAll('#index-page .tag').forEach(e => e.addEventListener("click", evt => {
// 如果所有标签都激活,则等效于所有都不激活
if (![...evt.srcElement.parentElement.children].filter(elem => elem.classList.contains('deactive')).length) {
[...evt.srcElement.parentElement.children].forEach(elem => elem.classList.add("deactive"));
}
// 切换标签激活状态
evt.srcElement.classList.toggle("deactive");
// 如果所有标签都不激活,则等效于所有都激活
if (![...evt.srcElement.parentElement.children].filter(elem => !elem.classList.contains('deactive')).length) {
[...evt.srcElement.parentElement.children].forEach(elem => elem.classList.remove("deactive"));
}
// 重置搜索页面数
pagecount = 0;
// 渲染新列表
renderProblemList()
}));

筛选功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function doSearch() {
return Object.entries(problems).filter(kv => {
let [pid, item] = kv;
// 忽略大小写搜索
if (document.getElementById("search").value && !(item.title + item.desc.replace(/<.*?>/g, '')).toLowerCase().includes(document.getElementById("search").value.toLowerCase())) {
return false;
}
// 按解出状态筛选
if (![...document.querySelectorAll("#solve-tags .tag:not(.deactive)")].map(e => e.innerText).includes(!(solveData['solved'][pid] || []).length ? '未解出' : (solveData['solved'][pid].length == item['flag'] ? '已解出' : '部分解出'))) {
return false;
}
// 按题目类型筛选
if (!item["tags"].filter(t => [...document.querySelectorAll("#category-tags .tag:not(.deactive)")].map(e => e.innerText).includes(t)).length) {
return false;
}
// 按题目特点筛选
if (!item["tags"].filter(t => [...document.querySelectorAll("#desc-tags .tag:not(.deactive)")].map(e => e.innerText).includes(t)).length) {
return false;
}
return true;
});
}

强兼博客代码样式

为了渲染WP中的内容(尤其是代码),我试了不少方式,最后还是决定手动解决WP渲染,就只剩下了一个code的highlight

经过搜索,我发现hexo的默认渲染引擎是highlight.js,并且默认移除hljs-前缀

我懒得修改hexo配置,于是就写了一段代码移除hljs-前缀,就可以使用博客主题分亮暗主题高亮了~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function removeHljsPrefix(elem) {
elem.classList.forEach(i => {
if (i.startsWith('hljs-')) {
elem.classList.replace(i, i.slice(5));
}
});
[...elem.children].forEach(sub => removeHljsPrefix(sub));
}

// 在渲染问题详情时
hljs.highlightAll();
setTimeout(() => document.querySelectorAll('pre code').forEach(
elem => {
elem.classList.add("code");
elem.parentElement.classList.add("highlight");
removeHljsPrefix(elem);
}
), 10)

hook <details> & <a>

为了在打开折叠框/下载黑盒文件前弹窗,对这两个元素进行了click事件的监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function hookDetailsOpen(elem) {
elem.addEventListener("toggle", async (evt) => {
if (elem.open && (!elem.dataset.confirmed || elem.dataset.confirmed == 'false')) { // 未确认过
elem.open = false; // 将打开的details关闭
if (await showConfirm('折叠框内含有与解题思路相关度较高的内容,您确定要展开吗?', 'Spoiler Alert!')) {
elem.dataset.confirmed = true; // 确认后记录
let arr = solveData['confirmed'][currentDisplayedPid] || [];
arr.push(elem.dataset.confirmId);
solveData['confirmed'][currentDisplayedPid] = arr;
saveData();
elem.open = true; // 重新打开
}
}
})
}

document.querySelectorAll("#problem-page details").forEach(elem => hookDetailsOpen(elem));
document.querySelectorAll("a[download][data-blackbox=true]").forEach(e => e.addEventListener("click", evt => {
evt.preventDefault(); // 阻止默认下载
// 弹窗提示
showAlert('这是一个黑盒附件,请勿在解题时阅读其中内容!', '黑盒附件提醒', '!!', '我知道了', { danger: true }).then(() => {
const tempA = document.createElement('a');
tempA.href = evt.srcElement.href;
tempA.download = evt.srcElement.download;
tempA.click(); // 创建临时元素并点击下载
tempA.remove();
})
}))

好了,其实代码里还是有很多细节的,欢迎查看源码哦~

2026/4/21: 更多窗口支持地址栏回退

在每咕一题中,题目页是支持地址栏回退的,这使得移动端可以通过返回按钮(或等效手势)退出问题页。

然而其他窗口对于这种操作并不支持,于是基于又爱又恨的HistoryAPI,在尽可能少改动代码的前提下,为弹出modal增加这一功能。

我选择了使用popstate来完成,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let popStateHandle = null;
function registerPopStateHandle(handle) {
popStateHandle = handle;
history.pushState(null, null, '');
}

function cleanupPopStateHandle(){
if(popStateHandle){
popStateHandle = null;
history.go(-1);
}
}

window.addEventListener("popstate", ()=>{
if(popStateHandle){
const handle = popStateHandle;
popStateHandle = null;
handle();
}
})
  • popStateHandle是注册的返回键按下时触发的函数,用于关闭窗口
  • registerPopStateHandle是一个帮助函数,集成了注册handle和pushState两步
  • cleanupPopStateHandle是一个帮助函数,用于在handle未触发时清理handle和历史栈
  • 最后的addEventListener注册监听函数,当注册了handle时,则调用它,注意这里清除handle和调用handle的顺序,先清除再调用使得handle中会调用cleanup的情况下不会过度清理历史栈

使用时,在弹出窗口处通过registerPopStateHandle注册handle,并在所有关闭窗口的地方(或者最后关闭窗口逻辑中)加入cleanupPopStateHandle来确保不污染历史栈和下一次操作,这样就可以通过返回键关闭窗口了~

2026/5/4: 若干小更新

回到学校里,捣腾一下博客

华丽切换动画

看到卡纳德的博客的主题切换动画,感觉挺有意思的,遂询问AI,得到是view-transition特性,于是查找文档成功复刻

核心参照的是MDN的这篇教程

简单修改之后,就完成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function switchColorScheme4Real(scheme){  // 这个就是原来的switchColorScheme函数
localStorage.setItem("color-scheme", scheme)
if(scheme==='light'){
document.getElementById("sun").style.display = "none"
document.getElementById("moon").style.display = ""
let classlist = document.querySelector("html").classList
classlist.remove('night')
classlist.add('light')
}else{
document.getElementById("sun").style.display = ""
document.getElementById("moon").style.display = "none"
let classlist = document.querySelector("html").classList
classlist.add('night')
classlist.remove('light')
}
}
function switchColorScheme(scheme){
if(!document.startViewTransition){
switchColorScheme4Real(scheme)
return
}
const x = innerWidth
const y = innerHeight
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
document.startViewTransition(()=>switchColorScheme4Real(scheme)).ready.then(() => {
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
]
},
{
duration: 400,
easing: "ease-in",
pseudoElement: "::view-transition-new(root)",
},
);
})
}

然后觉得这么花的动画可能有人不喜欢,CSS规范里也考虑到了这一点,有一个媒体查询(prefers-reduced-motion),不过我印象里没看过这个选项,那就往feature页加一个好啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
/* 样式较为复杂,从略 */
</style>

<fieldset>
<legend>主题切换动画</legend>
<div style="display: flex; flex-wrap: wrap;">
<button class="theme-change-opt" value="fancy">使用华丽的切换动画</button>
<button class="theme-change-opt" value="plain">使用朴素的切换动画</button>
<button class="theme-change-opt" value="media" title="遵循@media (prefers-reduced-motion)的结果">使用媒体查询的结果</button>
</div>
</fieldset>

<script>
document.querySelectorAll(".theme-change-opt").forEach(elem => {
elem.addEventListener("click", ((elem)=>()=>{
localStorage.setItem('reduce-motion', elem.value);
document.querySelectorAll(".theme-change-opt").forEach(e=>e.disabled = false);
elem.disabled = true;
})(elem))
})
document.querySelector(`.theme-change-opt[value=${localStorage.getItem('reduce-motion')??"media"}]`).disabled = true;
</script>

还是要翻文档,原来MDN里面有提到在哪里设置

然后修改一下switchColorScheme

1
2
3
4
5
6
7
8
9
prefersReducedMotion = localStorage.getItem('reduce-motion') ? {fancy: false, plain: true, media: matchMedia('(prefers-reduced-motion)').matches}[localStorage.getItem('reduce-motion')] : matchMedia('(prefers-reduced-motion)').matches
function switchColorScheme(scheme){
if(!document.startViewTransition || prefersReducedMotion){
switchColorScheme4Real(scheme)
return
}

// 后面就是一样的了,从略
}

不同页面不刷新同步设置

简单来说就是window.addEventListener("focus", ...)来监听localStorage的变化,然后修改页面

以过场动画的设置项为例:

在使用时,直接从localStorage取值以避免使用旧值

1
2
3
4
5
6
function switchColorScheme(scheme){
// 将计算从预计算改为使用时计算
prefersReducedMotion = localStorage.getItem('reduce-motion') ? {fancy: false, plain: true, media: matchMedia('(prefers-reduced-motion)').matches}[localStorage.getItem('reduce-motion')] : matchMedia('(prefers-reduced-motion)').matches

// 后面就是一样的了,从略
}

在feature页,通过监听来更新选项

1
2
3
4
window.addEventListener("focus", ()=>{
document.querySelectorAll(".theme-change-opt").forEach(e=>e.disabled = false);
document.querySelector(`.theme-change-opt[value=${localStorage.getItem('reduce-motion')??"media"}]`).disabled = true;
})

其他选项也是同理

更优返回按钮选项

在深入唾骂了解historyAPI之后,我终于写出了两种相对较优的返回按钮方案,原理比较简单

这两种都要对页内跳转锚点a[href^="#"]进行监听

一种是直接拦截默认行为,然后手动scrollIntoView,这种不会在历史中增加记录,成为新的默认值

另一种是拦截后增加计数器,记录回退次数,这种会增加历史条目,适合需要通过地址栏回退返回上一个锚点的用户使用

快去设置里调整一下优化体验吧!

使用Javascript动态添加按钮组边框

刚把博客推到github上,才发现feature页里的按钮组在手机上样式会炸掉

忘了你会wrap了.png

于是询问D老师,得知flex没有按行列的选择器,只好用js写一个了

不过D老师写的好乱,好几十行还一大堆注释,忍无可忍的我就自己写了一个

下面给出代码和注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<style>
.theme-change-opt.l {
border-left-width: 2px;
}
.theme-change-opt.t {
border-top-width: 2px;
}
.theme-change-opt.r {
border-right-width: 2px;
}
.theme-change-opt.b {
border-bottom-width: 2px;
}

.theme-change-opt.l.t {
border-top-left-radius: 10px;
}

.theme-change-opt.t.r {
border-top-right-radius: 10px;
}

.theme-change-opt.r.b {
border-bottom-right-radius: 10px;
}

.theme-change-opt.b.l {
border-bottom-left-radius: 10px;
}
</style>

<!-- HTML部分省略 -->

<script>

// 功能性JS代码省略

function updateBorder() {
const container = document.getElementById("theme-change-opt-group");
const bigRect = container.getBoundingClientRect(); // 父元素的框
document.querySelectorAll(".theme-change-opt").forEach(elem => {
elem.classList.remove("l", "t", "r", "b"); // 清除先前样式
const rect = elem.getBoundingClientRect(); // 子元素的框
for(let [ppt, cn] of [['top', 't'], ['right', 'r'], ['bottom', 'b'], ['left', 'l']]){ // 遍历属性及对应类名
if(Math.abs(bigRect[ppt] - rect[ppt]) < 5){ // 相差5px以内,认定为顶边
elem.classList.add(cn); // 添加对应样式
}
}
})
}

function requestUpdateBorder() { // 请求更新,使用requestAnimationFrame将频率限制在刷新率以下
let requested;
if(requested){return;}
requested = true;
requestAnimationFrame(()=>{
updateBorder();
requested = false;
})
}

(new ResizeObserver(updateBorder)).observe(document.getElementById("theme-change-opt-group")); // 使用ResizeObserver监听父元素变化
</script>

简洁多了

D老师的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Flex子元素动态边框与圆角 | 朝外边框 + 角落圆角</title>
<style>
/* 基础样式 - 仅用于演示环境,不影响核心逻辑 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: linear-gradient(145deg, #eef2f7 0%, #d9e2ec 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
padding: 2rem;
}

/* 演示卡片容器 */
.demo-card {
max-width: 1300px;
width: 100%;
background: rgba(255,255,255,0.55);
backdrop-filter: blur(2px);
border-radius: 2rem;
box-shadow: 0 25px 45px -12px rgba(0,0,0,0.3);
padding: 1.8rem;
transition: all 0.2s;
}

h1 {
font-size: 1.8rem;
font-weight: 600;
color: #0a1927;
letter-spacing: -0.3px;
margin-bottom: 0.3rem;
}

.desc {
color: #1e3a3a;
border-left: 4px solid #2c7a7b;
padding-left: 1rem;
margin-bottom: 1.8rem;
font-size: 0.9rem;
font-weight: 500;
}

/* ========= 核心演示:FLEX 容器 (子元素自动获取边框与圆角) ========= */
.flex-container {
display: flex;
flex-wrap: wrap;
/* 关键:无gap保证紧贴,边框逻辑依靠行列判断,内部无相邻边框干扰 */
gap: 0;
background-color: #fef9e8;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
/* 给容器极小内边距,仅用于视觉,不影响边框判断逻辑 (border基于子元素位置) */
padding: 0;
}

/* 所有子元素强制 flex: 1(核心条件),同时保证 box-sizing 使边框不改变外尺寸 */
.flex-container > * {
flex: 1;
box-sizing: border-box;
/* 基础样式:背景,文字居中,内边距舒适,不干扰边框显示 */
background-color: #ffffff;
color: #1e2f2f;
text-align: center;
padding: 1rem 0.75rem;
font-weight: 500;
font-size: 0.95rem;
transition: all 0.15s ease;
/* 移除默认边框,等待动态类添加 */
border-style: solid;
border-color: #1f5e5e;
border-width: 0px;
/* 圆角初始为0,后续由组合类动态生成 */
border-radius: 0;
/* 保证圆润视觉,背景不溢出 */
background-clip: padding-box;
}

/* 鼠标悬停提升层次感 (不影响边框逻辑) */
.flex-container > *:hover {
background-color: #fff7e8;
transform: translateY(-1px);
box-shadow: 0 6px 12px rgba(0,0,0,0.05);
z-index: 2;
position: relative;
}

/* ========= 边框类:分别控制四个方向 2px solid ========= */
.flex-container > .l {
border-left-width: 2px;
}
.flex-container > .t {
border-top-width: 2px;
}
.flex-container > .r {
border-right-width: 2px;
}
.flex-container > .b {
border-bottom-width: 2px;
}

/* ========= 角部圆角规则 (同时拥有两个相邻边框时圆角生效) ========= */
/* 左上角:同时有左边框和上边框 */
.flex-container > .l.t {
border-top-left-radius: 10px;
}
/* 右上角:同时有上边框和右边框 */
.flex-container > .t.r {
border-top-right-radius: 10px;
}
/* 右下角:同时有右边框和下边框 */
.flex-container > .r.b {
border-bottom-right-radius: 10px;
}
/* 左下角:同时有下边框和左边框 */
.flex-container > .b.l {
border-bottom-left-radius: 10px;
}

/* 额外细节:如果同一个按钮同时拥有四个边框(单一子元素场景),四个圆角全部自动生效 */
/* 为了更好的视觉,可选统一内部文字风格 */
.flex-container > * {
word-break: keep-all;
white-space: nowrap;
}
/* 窄屏时允许按钮文字换行,避免挤压, 不影响边框判断 */
@media (max-width: 640px) {
.flex-container > * {
white-space: normal;
word-break: break-word;
padding: 0.8rem 0.5rem;
font-size: 0.8rem;
}
}

/* 右下角提示信息区域 */
.info-panel {
margin-top: 1.8rem;
background: #e9f0e6;
border-radius: 1rem;
padding: 1rem 1.2rem;
font-size: 0.85rem;
color: #1f3d3a;
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
}
.badge {
background: #2c7a7b;
color: white;
padding: 0.2rem 0.7rem;
border-radius: 20px;
font-size: 0.7rem;
font-weight: 600;
}
button {
background: #2c7a7b;
border: none;
color: white;
padding: 0.4rem 1rem;
border-radius: 2rem;
font-weight: 500;
cursor: pointer;
transition: 0.1s;
font-family: inherit;
}
button:hover {
background: #1f5e5e;
transform: scale(0.96);
}
.resize-note {
font-family: monospace;
font-size: 0.7rem;
}
hr {
margin: 12px 0;
border-color: #cbd5e1;
}
</style>
</head>
<body>
<div class="demo-card">
<h1>🔲 动态朝外边框 · 智能圆角</h1>
<div class="desc">
Flex 子元素(<code>flex: 1</code>)自动添加 <strong>l / t / r / b</strong> 类 → 仅显示组外边框,内部无重叠边框。<br>
<code>.l.t</code><code>.t.r</code><code>.r.b</code><code>.b.l</code> 自动产生 <strong>10px 圆角</strong>。窗口缩放或换行时实时更新。
</div>

<!-- 核心 Flex 容器,所有直接子元素均拥有 flex: 1(通过 CSS 通配继承) -->
<div class="flex-container" id="dynamicFlexContainer">
<div>首页</div>
<div>产品方案</div>
<div>技术文档</div>
<div>开发者社区</div>
<div>支持与服务</div>
<div>关于我们</div>
<div>最新活动</div>
<div>资源中心</div>
</div>

<div class="info-panel">
<span>✅ 子元素自动获得 <strong>朝外侧边框</strong>(上下左右依据行列)</span>
<span>🎯 四角圆角(左上/右上/右下/左下)仅在相邻边框同时出现时生效</span>
<button id="refreshBtn">🔄 强制刷新边框布局</button>
</div>
<div class="resize-note" style="margin-top: 12px; text-align: right; color:#2c5a5a;">
💡 提示: 拖拽浏览器窗口宽度 → Flex 自动换行 → 边框与圆角动态适配 (ResizeObserver + 防抖)
</div>
</div>

<script>
(function() {
// ---------- 核心配置 ----------
// 用户指定选择器 (支持任意 flex 容器,需确保子元素有 flex: 1)
const CONTAINER_SELECTOR = '#dynamicFlexContainer'; // 可修改为任何 flex 容器选择器

// 获取容器元素
const container = document.querySelector(CONTAINER_SELECTOR);
if (!container) {
console.error(`未找到容器: ${CONTAINER_SELECTOR}`);
return;
}

// 确保容器是 flex 布局 (仅用于合理性检查,不强制阻止)
const computedStyle = window.getComputedStyle(container);
if (computedStyle.display !== 'flex' && !computedStyle.display.includes('flex')) {
console.warn('容器未设置 display: flex,边框逻辑可能不准确,请添加 flex 布局');
}

// 辅助函数:获取所有直接子元素(仅元素节点,且不隐藏)
function getChildElements(parent) {
return Array.from(parent.children).filter(el => {
// 只过滤掉非元素节点(children 本就是元素),额外可排除 display: none 或隐藏元素
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'collapse';
});
}

// 移除给定元素上的所有边框类 (l, t, r, b)
function removeBorderClasses(el) {
el.classList.remove('l', 't', 'r', 'b');
}

// 核心更新函数:计算行列位置,为每个子元素添加正确的 l/t/r/b 类
function updateDynamicBorders() {
const children = getChildElements(container);
if (children.length === 0) return;

// 1. 获取每个子元素相对于容器的边界信息 (top/left/right/bottom)
const containerRect = container.getBoundingClientRect();
const itemsData = children.map(el => {
const rect = el.getBoundingClientRect();
return {
el: el,
top: rect.top - containerRect.top,
left: rect.left - containerRect.left,
right: rect.right - containerRect.left,
bottom: rect.bottom - containerRect.top,
width: rect.width,
height: rect.height
};
});

// 容差 (处理亚像素渲染以及细微高度不一致)
const TOLERANCE = 2.5;

// 2. 按 top 值分组 -> 确定行 (rows)
const sortedByTop = [...itemsData].sort((a, b) => a.top - b.top);
const rows = [];
let currentRow = [];
for (let i = 0; i < sortedByTop.length; i++) {
const item = sortedByTop[i];
if (currentRow.length === 0) {
currentRow.push(item);
} else {
const firstTop = currentRow[0].top;
if (Math.abs(item.top - firstTop) <= TOLERANCE) {
currentRow.push(item);
} else {
// 当前行结束,按 left 排序后存入 rows
currentRow.sort((a, b) => a.left - b.left);
rows.push([...currentRow]);
currentRow = [item];
}
}
}
if (currentRow.length) {
currentRow.sort((a, b) => a.left - b.left);
rows.push(currentRow);
}

// 3. 清除所有子元素现有的 l/t/r/b 类
children.forEach(child => removeBorderClasses(child));

// 4. 根据行列判定添加边框类
// 4.1 每一行的第一个元素加左边框(l),最后一个元素加右边框(r)
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
const row = rows[rowIdx];
if (row.length === 0) continue;
// 最左侧元素加左边界
const firstItem = row[0];
firstItem.el.classList.add('l');
// 最右侧元素加右边界
const lastItem = row[row.length - 1];
lastItem.el.classList.add('r');
}

// 4.2 顶部边框: 第一行所有元素加 t
if (rows.length > 0) {
const firstRow = rows[0];
firstRow.forEach(item => {
item.el.classList.add('t');
});
}

// 4.3 底部边框: 最后一行所有元素加 b
if (rows.length > 0) {
const lastRow = rows[rows.length - 1];
lastRow.forEach(item => {
item.el.classList.add('b');
});
}

// 注意: 由于我们基于行列分析,不会出现内部相邻边框重叠问题,因为左右边框只给每行首尾,上下边框只给首尾行。
// 圆角会自动通过 CSS 组合 (.l.t , .t.r , .r.b , .b.l) 生效,无需额外处理。
// 特别场景: 只有一个子元素时,它同时获得 l, t, r, b,四个圆角组合类将分别生成四个10px圆角。
}

// ---------- 性能优化 & 监听布局/样式变化 ----------
let pendingUpdate = false;
const scheduleUpdate = () => {
if (pendingUpdate) return;
pendingUpdate = true;
requestAnimationFrame(() => {
updateDynamicBorders();
pendingUpdate = false;
});
};

// 防抖函数 (用于窗口resize)
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}

const debouncedResize = debounce(() => {
scheduleUpdate();
}, 100);

// 监听窗口大小变化
window.addEventListener('resize', debouncedResize);

// 使用 ResizeObserver 监听容器尺寸变化 (当flex换行导致高度变化等)
let resizeObserver = null;
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(() => {
scheduleUpdate();
});
if (container) {
resizeObserver.observe(container);
}
}

// 监听子元素变化 (增删、文本内容变化可能影响宽度布局)
const mutationObserver = new MutationObserver((mutations) => {
let needUpdate = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
needUpdate = true;
break;
}
if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class')) {
// 避免由于我们自己的class修改引起死循环,不过我们只添加类名,不会触发子元素class变化再次导致布局改变?
// 子元素class变化不会影响元素位置尺寸,但还是小心避免无意义触发。一般情况安全。
// 为了性能,只排除可能改变布局的属性,如宽高等,但通常children宽高变化主要由resize处理。
// 为了鲁棒,当子元素自身样式可能影响尺寸时也触发一次更新 (但不会导致无限递归,因为更新不会再次触发属性变更)
if (mutation.target !== container) {
needUpdate = true;
}
}
}
if (needUpdate) {
scheduleUpdate();
}
});
mutationObserver.observe(container, { childList: true, subtree: false, attributes: true, attributeFilter: ['style', 'class'] });

// 页面加载完成后立即执行一次
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
scheduleUpdate();
});
} else {
scheduleUpdate();
}

// 额外增强:字体加载或图片加载可能引起微偏移,延迟再执行一次保证稳定
window.addEventListener('load', () => {
scheduleUpdate();
setTimeout(scheduleUpdate, 150);
});

// 可选:监听 orientation change 以及滚动条事件(滚动不影响布局,但忽略)
window.addEventListener('orientationchange', () => {
setTimeout(scheduleUpdate, 80);
});

// 手动刷新按钮 (demo 交互)
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
scheduleUpdate();
// 视觉反馈
refreshBtn.textContent = '✨ 已刷新';
setTimeout(() => {
refreshBtn.textContent = '🔄 强制刷新边框布局';
}, 800);
});
}

// 动态演示: 控制台可调用 window.__refreshFlexBorders 手动更新
window.__refreshFlexBorders = scheduleUpdate;

// 监听也许子元素内文本变化(例如通过js动态修改), MutationObserver已经覆盖 childList, 但文本变化不产生节点变化但可能让元素宽度变化
// 极端情况,使用一个较小的心跳?不推荐,但可以通过ResizeObserver完全覆盖子元素尺寸变动。由于子元素尺寸变动会触发容器ResizeObserver,同样会重新计算。
// 因此完整覆盖。
console.log('✅ 动态边框脚本已启动 — 根据行列自动为flex子元素分配 l/t/r/b 类,并组合圆角');
})();
</script>

<!-- 额外确保示例中的子元素都具有 flex:1,上述 CSS 已保证 .flex-container > * 有 flex:1 -->
<!-- 本实现完全满足需求:selector 容器下的子元素动态获取 l,t,r,b 类;四角圆角条件性生效。且仅显示朝外边框,内部无重叠。 -->
</body>
</html>

这样就好了~

修复完成

欧,应该把borderborder-radiustransition里面排除掉才对

再修一下……