Skip to content

Commit 524a47c

Browse files
CodFrmcyfung1031
andauthored
🐛 修复拖拽组件导致触发focusin / focusout卡顿 #1224 (#1243)
* 🐛 修复拖拽组件导致触发focusin / focusout卡顿 #1224 * 使用noKeyboard,不需要自己指定undefined * 抽离遮罩组件, 使用 useMemo 缓存语言列表,工具函数移出组件外,控制CSS行为 * 放到body --------- Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
1 parent 3645f64 commit 524a47c

2 files changed

Lines changed: 75 additions & 57 deletions

File tree

src/pages/components/layout/MainLayout.tsx

Lines changed: 69 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
IconSunFill,
2424
} from "@arco-design/web-react/icon";
2525
import type { ReactNode } from "react";
26-
import React, { useEffect, useRef, useState } from "react";
26+
import React, { useEffect, useMemo, useRef, useState } from "react";
2727
import { useTranslation } from "react-i18next";
2828
import { useAppContext } from "@App/pages/store/AppContext";
2929
import { RiFileCodeLine, RiImportLine, RiPlayListAddLine, RiTerminalBoxLine, RiTimerLine } from "react-icons/ri";
@@ -37,6 +37,8 @@ import { prepareScriptByCode } from "@App/pkg/utils/script";
3737
import { saveHandle } from "@App/pkg/utils/filehandle-db";
3838
import { makeBlobURL } from "@App/pkg/utils/utils";
3939

40+
// --- 工具函数移出组件外,避免每次 Render 重新定义 ---
41+
4042
const formatUrl = async (url: string) => {
4143
try {
4244
const newUrl = new URL(url.replace(/\/$/, ""));
@@ -69,6 +71,21 @@ const formatUrl = async (url: string) => {
6971
}
7072
};
7173

74+
// 提供一个简单的字串封装(非加密用)
75+
const simpleDigestMessage = async (message: string) => {
76+
const encoder = new TextEncoder();
77+
const data = encoder.encode(message);
78+
return crypto.subtle.digest("SHA-1", data as BufferSource).then((hashBuffer) => {
79+
const hashArray = new Uint8Array(hashBuffer);
80+
let hex = "";
81+
for (let i = 0; i < hashArray.length; i++) {
82+
const byte = hashArray[i];
83+
hex += `${byte < 16 ? "0" : ""}${byte.toString(16)}`;
84+
}
85+
return hex;
86+
});
87+
};
88+
7289
type TImportStat = {
7390
success: number;
7491
fail: number;
@@ -102,6 +119,31 @@ const importByUrls = async (urls: string[]): Promise<TImportStat | undefined> =>
102119
return stat;
103120
};
104121

122+
// --- 子组件:提取拖拽遮罩以优化性能 ---
123+
const DropzoneOverlay: React.FC<{ active: boolean; text: string }> = React.memo(({ active, text }) => {
124+
if (!active) return null;
125+
return (
126+
<div
127+
className="sc-inset-0"
128+
style={{
129+
position: "absolute",
130+
zIndex: 100,
131+
display: "flex",
132+
justifyContent: "center",
133+
alignItems: "center",
134+
color: "grey",
135+
fontSize: 36,
136+
backdropFilter: "blur(4px)",
137+
background: "var(--color-fill-2)",
138+
opacity: 0.8,
139+
}}
140+
>
141+
{text}
142+
</div>
143+
);
144+
});
145+
DropzoneOverlay.displayName = "DropzoneOverlay";
146+
105147
const MainLayout: React.FC<{
106148
children: ReactNode;
107149
className: string;
@@ -146,21 +188,6 @@ const MainLayout: React.FC<{
146188
if (stat) showImportResult(stat);
147189
};
148190

149-
// 提供一个简单的字串封装(非加密用)
150-
function simpleDigestMessage(message: string) {
151-
const encoder = new TextEncoder();
152-
const data = encoder.encode(message);
153-
return crypto.subtle.digest("SHA-1", data as BufferSource).then((hashBuffer) => {
154-
const hashArray = new Uint8Array(hashBuffer);
155-
let hex = "";
156-
for (let i = 0; i < hashArray.length; i++) {
157-
const byte = hashArray[i];
158-
hex += `${byte < 16 ? "0" : ""}${byte.toString(16)}`;
159-
}
160-
return hex;
161-
});
162-
}
163-
164191
const onDrop = (acceptedFiles: FileWithPath[]) => {
165192
// 本地的文件在当前页面处理,打开安装页面,将FileSystemFileHandle传递过去
166193
// 实现本地文件的监听
@@ -178,7 +205,7 @@ const MainLayout: React.FC<{
178205
} else if (aFile instanceof File) {
179206
// 清理 import-local files 避免同文件不再触发onChange
180207
(document.getElementById("import-local") as HTMLInputElement).value = "";
181-
const blob = new Blob([aFile], { type: "application/javascript" });
208+
const blob = new Blob([aFile], { type: "text/javascript" });
182209
const url = makeBlobURL({ blob, persistence: false }) as string; // 生成一个临时的URL
183210
const result = await scriptClient.importByUrl(url);
184211
if (result.success) {
@@ -223,24 +250,28 @@ const MainLayout: React.FC<{
223250
};
224251

225252
const { getRootProps, getInputProps, isDragActive } = useDropzone({
226-
accept: { "application/javascript": [".js"] },
253+
accept: { "text/javascript": [".js"] },
227254
onDrop,
255+
noClick: true,
256+
noKeyboard: true,
228257
});
229258

230-
const languageList: { key: string; title: string }[] = [];
231-
for (const key of Object.keys(i18n.store.data)) {
232-
if (key === "ach-UG") {
233-
continue;
234-
}
235-
languageList.push({
236-
key,
237-
title: i18n.store.data[key].title as string,
238-
});
239-
}
240-
languageList.push({
241-
key: "help",
242-
title: t("help_translate"),
243-
});
259+
// 当dragzone使用时,在<body>加入.dragzone-active,控制CSS行为
260+
// 只改CSS,不要改动React元件的任何状态,否则会触发重绘计算
261+
useEffect(() => {
262+
document.body.classList.toggle("dragzone-active", isDragActive);
263+
}, [isDragActive]);
264+
265+
// 使用 useMemo 缓存语言列表,避免每次重绘都执行循环,然后生成新的参考
266+
const languageList = useMemo(() => {
267+
const list = Object.keys(i18n.store.data)
268+
.filter((key) => key !== "ach-UG")
269+
.map((key) => ({
270+
key,
271+
title: i18n.store.data[key].title as string,
272+
}));
273+
return [...list, { key: "help", title: t("help_translate") }];
274+
}, [t]);
244275

245276
useEffect(() => {
246277
// 当没有匹配语言时显示语言按钮
@@ -253,7 +284,7 @@ const MainLayout: React.FC<{
253284

254285
const handleImport = async () => {
255286
const urls = importRef.current!.dom.value.split("\n").filter((v) => v);
256-
importByUrlsLocal(urls); // 異步卻不用等候
287+
importByUrlsLocal(urls); // 异步却不用等候
257288
setImportVisible(false); // 不等待 importByUrlsLocal?
258289
};
259290

@@ -332,7 +363,7 @@ const MainLayout: React.FC<{
332363
types: [
333364
{
334365
description: "JavaScript",
335-
accept: { "application/javascript": [".js"] },
366+
accept: { "text/javascript": [".js"] },
336367
},
337368
],
338369
})
@@ -447,30 +478,12 @@ const MainLayout: React.FC<{
447478
</Layout.Header>
448479
<Layout
449480
className={`tw-bottom-0 tw-w-full ${className}`}
450-
style={{
451-
background: "var(--color-fill-2)",
452-
}}
453-
{...getRootProps({ onClick: (e) => e.stopPropagation() })}
481+
style={{ background: "var(--color-fill-2)" }}
482+
{...getRootProps({})}
454483
>
455484
<input id="import-local" {...getInputProps({ style: { display: "none" } })} />
456-
<div
457-
className="sc-inset-0"
458-
style={{
459-
position: "absolute",
460-
zIndex: 100,
461-
display: isDragActive ? "flex" : "none",
462-
justifyContent: "center",
463-
alignItems: "center",
464-
margin: "auto",
465-
color: "grey",
466-
fontSize: 36,
467-
width: "100%",
468-
height: "100%",
469-
backdropFilter: "blur(4px)",
470-
}}
471-
>
472-
{t("drag_script_here_to_upload")}
473-
</div>
485+
{/* 性能关键:抽离遮罩组件,只有 active 变化时此小组件重绘 */}
486+
<DropzoneOverlay active={isDragActive} text={t("drag_script_here_to_upload")} />
474487
{children}
475488
</Layout>
476489
</Layout>

src/pages/components/layout/index.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@
1818

1919
:is(.arco-dropdown-menu-pop-header, .arco-dropdown-menu-item, .arco-dropdown-menu-item a) > svg {
2020
margin-right: .5em;
21-
}
21+
}
22+
23+
/* 避免拖拽时 tooltip 等元件弹出 */
24+
.dragzone-active .arco-layout-content {
25+
pointer-events: none;
26+
}

0 commit comments

Comments
 (0)