Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scriptcat",
"version": "1.2.0-beta.5",
"version": "1.2.0",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm",
"license": "GPLv3",
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_scriptcat__",
"version": "1.2.0.1600",
"version": "1.2.0",
"author": "CodFrm",
"description": "__MSG_scriptcat_description__",
"options_ui": {
Expand Down
14 changes: 4 additions & 10 deletions src/pages/components/ScriptResource/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@ import { ResourceClient } from "@App/app/service/service_worker/client";
import { message } from "@App/pages/store/global";
import { base64ToBlob } from "@App/pkg/utils/utils";
import { Button, Drawer, Input, Message, Popconfirm, Space, Table } from "@arco-design/web-react";
import type { RefInputType } from "@arco-design/web-react/es/Input/interface";
import type { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconDelete, IconDownload, IconSearch } from "@arco-design/web-react/icon";
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";

type ResourceListItem = {
key: string;
} & Resource;

const resourceClient = new ResourceClient(message);

const ScriptResource: React.FC<{
script?: Script;
visible: boolean;
onOk: () => void;
onCancel: () => void;
}> = ({ script, visible, onCancel, onOk }) => {
const [data, setData] = useState<ResourceListItem[]>([]);
const inputRef = useRef<RefInputType>(null);
const { t } = useTranslation();
const resourceClient = new ResourceClient(message);

useEffect(() => {
if (!script) {
Expand Down Expand Up @@ -53,8 +52,8 @@ const ScriptResource: React.FC<{
return (
<div className="arco-table-custom-filter">
<Input.Search
ref={inputRef}
searchButton
autoFocus
placeholder={t("enter_key")!}
value={filterKeys[0] || ""}
onChange={(value) => {
Expand All @@ -68,11 +67,6 @@ const ScriptResource: React.FC<{
);
},
onFilter: (value, row) => !value || row.key.includes(value),
onFilterDropdownVisibleChange: (v) => {
if (v) {
setTimeout(() => inputRef.current!.focus(), 1);
}
},
},
{
title: t("type"),
Expand Down
11 changes: 2 additions & 9 deletions src/pages/components/ScriptStorage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import type { Script } from "@App/app/repo/scripts";
import { valueClient } from "@App/pages/store/features/script";
import { valueType } from "@App/pkg/utils/utils";
import { Button, Drawer, Form, Input, Message, Modal, Popconfirm, Select, Space, Table } from "@arco-design/web-react";
import type { RefInputType } from "@arco-design/web-react/es/Input/interface";
import type { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconDelete, IconEdit, IconSearch } from "@arco-design/web-react/icon";
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";

const FormItem = Form.Item;
Expand All @@ -23,7 +22,6 @@ const ScriptStorage: React.FC<{
}> = ({ script, visible, onCancel, onOk }) => {
const [data, setData] = useState<ValueModel[]>([]);
const [rawData, setRawData] = useState<{ [key: string]: any }>({});
const inputRef = useRef<RefInputType>(null);
const [currentValue, setCurrentValue] = useState<ValueModel>();
const [visibleEdit, setVisibleEdit] = useState(false);
const [isEdit, setIsEdit] = useState(false);
Expand Down Expand Up @@ -93,8 +91,8 @@ const ScriptStorage: React.FC<{
return (
<div className="arco-table-custom-filter">
<Input.Search
ref={inputRef}
searchButton
autoFocus
placeholder={t("enter_key")!}
value={filterKeys[0] || ""}
onChange={(value) => {
Expand All @@ -108,11 +106,6 @@ const ScriptStorage: React.FC<{
);
},
onFilter: (value, row) => !value || row.key.includes(value),
onFilterDropdownVisibleChange: (v) => {
if (v) {
setTimeout(() => inputRef.current!.focus(), 1);
}
},
},
{
title: t("value"),
Expand Down
32 changes: 14 additions & 18 deletions src/pages/options/routes/ScriptList/ScriptTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import {
Avatar,
Button,
Expand Down Expand Up @@ -38,7 +38,6 @@ import {
RiUploadCloudFill,
} from "react-icons/ri";
import { Link, useNavigate } from "react-router-dom";
import type { RefInputType } from "@arco-design/web-react/es/Input/interface";
import Text from "@arco-design/web-react/es/Typography/text";
import type { DragEndEvent } from "@dnd-kit/core";
import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
Expand All @@ -60,8 +59,7 @@ import { requestEnableScript, pinToTop, scriptClient, synchronizeClient } from "
import { getCombinedMeta } from "@App/app/service/service_worker/utils";
import { parseTags } from "@App/app/repo/metadata";
import { EnableSwitch, HomeCell, MemoizedAvatar, ScriptSearchField, SourceCell, UpdateTimeCell } from "./components";
import type { SetSearchRequest } from "./hooks";
import type { SearchType } from "@App/app/service/service_worker/types";
import { SearchFilter } from "./SearchFilter";

type ListType = ScriptLoading;

Expand Down Expand Up @@ -420,8 +418,6 @@ interface ScriptTableProps {
updateScripts: (uuids: string[], data: Partial<Script | ScriptLoading>) => void;
setUserConfig: (config: { script: Script; userConfig: UserConfig; values: { [key: string]: any } }) => void;
setCloudScript: (script: Script) => void;
searchRequest: { keyword: string; type: SearchType };
setSearchRequest: SetSearchRequest;
handleDelete: (item: ScriptLoading) => void;
handleConfig: (
item: ScriptLoading,
Expand All @@ -440,8 +436,6 @@ export const ScriptTable = ({
updateScripts,
setUserConfig,
setCloudScript,
searchRequest,
setSearchRequest,
handleDelete,
handleConfig,
handleRunStop,
Expand All @@ -451,7 +445,6 @@ export const ScriptTable = ({
const [action, setAction] = useState("");
const [select, setSelect] = useState<Script[]>([]);
const [selectColumn, setSelectColumn] = useState(0);
const inputRef = useRef<RefInputType>(null);
const navigate = useNavigate();
const [savedWidths, setSavedWidths] = useState<{ [key: string]: number } | null>(null);

Expand Down Expand Up @@ -494,25 +487,30 @@ export const ScriptTable = ({
dataIndex: "name",
sorter: (a, b) => a.name.localeCompare(b.name),
filterIcon: <IconSearch />,
filterDropdown: ({ confirm }: any) => {
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
return (
<div className="arco-table-custom-filter flex flex-row gap-2">
<ScriptSearchField
t={t}
defaultValue={searchRequest}
autoFocus
defaultValue={filterKeys?.[0] || { type: "auto", keyword: "" }}
onChange={(req) => {
setFilterKeys([{ type: req.type, keyword: req.keyword }]);
SearchFilter.requestFilterResult(req);
}}
onSearch={(req) => {
setSearchRequest(req);
if (req.bySelect) return;
confirm();
}}
inputRef={inputRef}
/>
</div>
);
},
onFilterDropdownVisibleChange: (visible) => {
if (visible) {
setTimeout(() => inputRef.current!.focus(), 1);
onFilter: (value, row) => {
if (!value || !value.keyword) {
return true;
}
return SearchFilter.checkByUUID(row.uuid);
},
className: "max-w-[240px] min-w-[100px]",
render: (col: string, item: ListType) => <NameCell col={col} item={item} />,
Expand Down Expand Up @@ -620,8 +618,6 @@ export const ScriptTable = ({
t,
sidebarOpen,
updateScripts,
searchRequest,
setSearchRequest,
navigate,
setSidebarOpen,
setViewMode,
Expand Down
50 changes: 50 additions & 0 deletions src/pages/options/routes/ScriptList/SearchFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { SearchType } from "@App/app/service/service_worker/types";
import { requestFilterResult } from "@App/pages/store/features/script";

export type SearchFilterKeyEntry = { type: SearchType; keyword: string };
export type SearchFilterRequest = { type: SearchType; keyword: string; bySelect?: boolean }; // 两个Type日后可能会不同。先分开写。

// 静态变量不随重绘重置
let lastReqType: SearchType | undefined = undefined;
let lastKeyword: string = "";
type SearchFilterCacheEntry = { code: boolean; name: boolean; auto: boolean };
const searchFilterCache: Map<string, SearchFilterCacheEntry> = new Map();

export class SearchFilter {
static async requestFilterResult(req: SearchFilterRequest) {
if (req.keyword === lastKeyword) {
lastReqType = req.type;
return Promise.resolve(this);
} else {
const res = await requestFilterResult({ value: req.keyword });
lastReqType = req.type;
lastKeyword = req.keyword;
searchFilterCache.clear();
if (res && Array.isArray(res)) {
for (const entry of res) {
searchFilterCache.set(entry.uuid, {
code: entry.code,
name: entry.name,
auto: entry.auto,
});
}
}
return this;
}
}

static checkByUUID(uuid: string): boolean {
const result = searchFilterCache.get(uuid);
if (!result) return false;
switch (lastReqType) {
case "auto":
return result.auto;
case "script_code":
return result.code;
case "name":
return result.name;
default:
return false;
}
}
Comment on lines +13 to +49
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新增的 SearchFilter 类缺少类和方法的文档注释。建议添加 JSDoc 注释来说明:

  1. 类的用途和设计模式(例如使用了模板方法模式)
  2. 各个方法的参数、返回值和副作用
  3. onResponse 方法的回调时机

例如:

/**
 * SearchFilter 类用于处理脚本搜索过滤逻辑
 * 使用模块级缓存来避免重复的搜索请求
 * 子类可以重写 onResponse 方法来处理搜索结果
 */
export class SearchFilter {
  /**
   * 请求过滤结果
   * @param req 搜索请求,包含关键词和搜索类型
   */
  requestFilterResult(req: SearchFilterRequest): void {
    // ...
  }
  
  /**
   * 当搜索结果返回时的回调方法(模板方法)
   * 子类应重写此方法来处理搜索结果
   */
  onResponse(_req: SearchFilterRequest, _res: SearchFilterResponse): void {
    // ...
  }
}

Copilot uses AI. Check for mistakes.
}
94 changes: 48 additions & 46 deletions src/pages/options/routes/ScriptList/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ListHomeRender } from "../utils";
import { IconEdit, IconLink, IconUserAdd } from "@arco-design/web-react/icon";
import type { SearchType } from "@App/app/service/service_worker/types";
import type { TFunction } from "i18next";
import type { RefInputType } from "@arco-design/web-react/es/Input";
import type { SearchFilterKeyEntry, SearchFilterRequest } from "./SearchFilter";

export const EnableSwitch = React.memo(
({
Expand Down Expand Up @@ -184,50 +184,52 @@ UpdateTimeCell.displayName = "UpdateTimeCell";

interface ScriptSearchFieldProps {
t: TFunction<"translation", undefined>;
defaultValue: { keyword: string; type: SearchType };
onSearch?: (req: { keyword: string; type: SearchType }) => void;
inputRef?: React.RefObject<RefInputType>;
defaultValue?: SearchFilterKeyEntry;
onChange?: (req: SearchFilterRequest) => void;
onSearch?: (req: SearchFilterRequest) => void;
autoFocus?: boolean;
}

export const ScriptSearchField = React.memo(
({ t, defaultValue, onSearch, inputRef }: ScriptSearchFieldProps) => {
const [keyword, setKeyword] = React.useState(defaultValue?.keyword || "");
const [type, setType] = React.useState<SearchType>(defaultValue?.type || "auto");
return (
<Space direction="horizontal">
<Select
className="flex-1"
triggerProps={{ autoAlignPopupWidth: false, autoAlignPopupMinWidth: true, position: "bl" }}
size="small"
value={type}
onChange={(value) => {
setType(value as SearchType);
onSearch?.({ keyword, type: value as SearchType });
}}
>
<Select.Option value="auto">{t("auto")}</Select.Option>
<Select.Option value="name">{t("name")}</Select.Option>
<Select.Option value="script_code">{t("script_code")}</Select.Option>
</Select>
<Input.Search
ref={inputRef}
size="small"
searchButton
style={{ width: 280 }}
value={keyword}
placeholder={t("enter_search_value", { search: `${t("name")}/${t("script_code")}` })!}
onChange={(value) => {
setKeyword(value);
}}
onSearch={(value) => {
onSearch?.({ keyword: value, type });
}}
/>
</Space>
);
},
(prevProps, nextProps) => {
return prevProps.t === nextProps.t && prevProps.defaultValue === nextProps.defaultValue;
}
);
ScriptSearchField.displayName = "ScriptSearchField";
export const ScriptSearchField = ({ t, defaultValue, onChange, onSearch, autoFocus }: ScriptSearchFieldProps) => {
const [keyword, setKeyword] = React.useState(defaultValue?.keyword || "");
const [type, setType] = React.useState<SearchType>(defaultValue?.type || "auto");
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScriptSearchField 组件的本地状态(keywordtype)只在初始化时从 defaultValue 读取,但不会在 defaultValue 改变时更新。这可能导致组件显示过时的值。

建议使用 useEffect 来同步外部 prop 的变化:

const [keyword, setKeyword] = React.useState(defaultValue?.keyword || "");
const [type, setType] = React.useState<SearchType>(defaultValue?.type || "auto");

React.useEffect(() => {
  if (defaultValue?.keyword !== undefined) {
    setKeyword(defaultValue.keyword);
  }
  if (defaultValue?.type !== undefined) {
    setType(defaultValue.type);
  }
}, [defaultValue]);
Suggested change
const [type, setType] = React.useState<SearchType>(defaultValue?.type || "auto");
const [type, setType] = React.useState<SearchType>(defaultValue?.type || "auto");
React.useEffect(() => {
if (defaultValue?.keyword !== undefined) {
setKeyword(defaultValue.keyword);
}
if (defaultValue?.type !== undefined) {
setType(defaultValue.type);
}
}, [defaultValue]);

Copilot uses AI. Check for mistakes.
return (
<Space direction="horizontal">
<Select
className="flex-1"
triggerProps={{ autoAlignPopupWidth: false, autoAlignPopupMinWidth: true, position: "bl" }}
size="small"
value={type}
onChange={(value) => {
setType(value as SearchType);
onChange?.({ keyword, type: value as SearchType });
onSearch?.({ keyword, type: value as SearchType, bySelect: true });
}}
>
<Select.Option value="auto">{t("auto")}</Select.Option>
<Select.Option value="name">{t("name")}</Select.Option>
<Select.Option value="script_code">{t("script_code")}</Select.Option>
</Select>
<Input.Search
size="small"
searchButton
autoFocus={autoFocus}
style={{ width: 280 }}
value={keyword}
placeholder={
t("enter_search_value", {
search:
type === "auto" ? `${t("name")}/${t("script_code")}` : type === "name" ? t("name") : t("script_code"),
})!
}
onChange={(value) => {
onChange?.({ keyword: value, type });
setKeyword(value);
}}
onSearch={(value) => {
onSearch?.({ keyword: value, type, bySelect: false });
}}
/>
</Space>
);
};
Comment on lines +193 to +235
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

移除了 React.memo 优化可能会导致性能问题。ScriptSearchField 组件会在 filterDropdown 中被渲染(在 ScriptTable.tsx 中),而 filterDropdown 每次表格重新渲染时都会被调用。没有 React.memo,即使 props 没有变化,组件也会重新渲染。

建议恢复 React.memo 或使用 useCallback 优化回调函数,以避免不必要的重新渲染:

export const ScriptSearchField = React.memo(({ t, defaultValue, onChange, onSearch, autoFocus }: ScriptSearchFieldProps) => {
  // ... 组件实现
});

Copilot uses AI. Check for mistakes.
Loading
Loading