@@ -23,7 +23,7 @@ import {
2323 IconSunFill ,
2424} from "@arco-design/web-react/icon" ;
2525import type { ReactNode } from "react" ;
26- import React , { useEffect , useRef , useState } from "react" ;
26+ import React , { useEffect , useMemo , useRef , useState } from "react" ;
2727import { useTranslation } from "react-i18next" ;
2828import { useAppContext } from "@App/pages/store/AppContext" ;
2929import { RiFileCodeLine , RiImportLine , RiPlayListAddLine , RiTerminalBoxLine , RiTimerLine } from "react-icons/ri" ;
@@ -37,6 +37,8 @@ import { prepareScriptByCode } from "@App/pkg/utils/script";
3737import { saveHandle } from "@App/pkg/utils/filehandle-db" ;
3838import { makeBlobURL } from "@App/pkg/utils/utils" ;
3939
40+ // --- 工具函数移出组件外,避免每次 Render 重新定义 ---
41+
4042const 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+
7289type 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+
105147const 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 >
0 commit comments