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
4 changes: 2 additions & 2 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Script, ScriptCode, ScriptRunResource, TClientPageLoadInfo } from
import { type Resource } from "@App/app/repo/resource";
import { type Subscribe } from "@App/app/repo/subscribe";
import { type Permission } from "@App/app/repo/permission";
import type { InstallSource, ScriptMenu, ScriptMenuItem, SearchType, TBatchUpdateListAction } from "./types";
import type { InstallSource, ScriptMenu, ScriptMenuItem, TBatchUpdateListAction } from "./types";
import { Client } from "@Packages/message/client";
import type { MessageSend } from "@Packages/message/types";
import type PermissionVerify from "./permission_verify";
Expand Down Expand Up @@ -66,7 +66,7 @@ export class ScriptClient extends Client {
return this.doThrow("fetchInfo", uuid);
}

getFilterResult(req: { type: SearchType; value: string }): Promise<ScriptCode | undefined> {
getFilterResult(req: { value: string }): Promise<ScriptCode | undefined> {
return this.do("getFilterResult", req);
}

Expand Down
3 changes: 1 addition & 2 deletions src/app/service/service_worker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import {
BatchUpdateListActionCode,
type TBatchUpdateListAction,
UpdateStatusCode,
type SearchType,
type TBatchUpdateRecord,
} from "./types";
import { getSimilarityScore, ScriptUpdateCheck } from "./script_update_check";
Expand Down Expand Up @@ -552,7 +551,7 @@ export class ScriptService {
return true;
}

async getFilterResult(req: { type: SearchType; value: string }) {
async getFilterResult(req: { value: string }) {
const OPTION_CASE_INSENSITIVE = true;
const scripts = await this.scriptDAO.all();
const scriptCodes = await Promise.all(
Expand Down
42 changes: 14 additions & 28 deletions src/pages/options/routes/ScriptList/ScriptCard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
import React, { useMemo, useState } from "react";
import {
Avatar,
Button,
Card,
Divider,
Input,
Popconfirm,
Space,
Tag,
Tooltip,
Typography,
} from "@arco-design/web-react";
import React, { useMemo } from "react";
import { Avatar, Button, Card, Divider, Popconfirm, Space, Tag, Tooltip, Typography } from "@arco-design/web-react";
import { Link, useNavigate } from "react-router-dom";
import { IconClockCircle, IconDragDotVertical } from "@arco-design/web-react/icon";
import {
Expand All @@ -30,7 +19,7 @@ import { hashColor, ScriptIcons } from "../utils";
import { getCombinedMeta } from "@App/app/service/service_worker/utils";
import { parseTags } from "@App/app/repo/metadata";
import type { ScriptLoading } from "@App/pages/store/features/script";
import { EnableSwitch, HomeCell, MemoizedAvatar, SourceCell, UpdateTimeCell } from "./components";
import { EnableSwitch, HomeCell, MemoizedAvatar, ScriptSearchField, SourceCell, UpdateTimeCell } from "./components";
import { useTranslation } from "react-i18next";
import { VscLayoutSidebarLeft, VscLayoutSidebarLeftOff } from "react-icons/vsc";
import { FaThList } from "react-icons/fa";
Expand All @@ -39,6 +28,8 @@ import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from
import { rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useAppContext } from "@App/pages/store/AppContext";
import type { SetSearchRequest } from "./hooks";
import type { SearchType } from "@App/app/service/service_worker/types";

const { Text } = Typography;

Expand Down Expand Up @@ -337,7 +328,8 @@ interface ScriptCardProps {
updateScripts: (uuids: string[], data: Partial<Script | ScriptLoading>) => void;
setUserConfig: (config: { script: Script; userConfig: UserConfig; values: { [key: string]: any } }) => void;
setCloudScript: (script: Script) => void;
setSearchKeyword: (keyword: string) => void;
searchRequest: { keyword: string; type: SearchType };
setSearchRequest: SetSearchRequest;
handleDelete: (item: ScriptLoading) => void;
handleConfig: (
item: ScriptLoading,
Expand All @@ -356,12 +348,12 @@ export const ScriptCard = ({
updateScripts,
setUserConfig,
setCloudScript,
setSearchKeyword,
searchRequest,
setSearchRequest,
handleDelete,
handleConfig,
handleRunStop,
}: ScriptCardProps) => {
const [searchValue, setSearchValue] = useState<string>("");
const { t } = useTranslation();
const { guideMode } = useAppContext();

Expand Down Expand Up @@ -417,17 +409,11 @@ export const ScriptCard = ({
>
<div className="flex flex-row justify-between items-center" style={{ padding: "8px 0" }}>
<div className="flex-1">
<Input.Search
size="small"
searchButton
style={{ maxWidth: 400 }}
placeholder={t("enter_search_value", { search: `${t("name")}/${t("script_code")}` })!}
value={searchValue}
onChange={(value) => {
setSearchValue(value);
}}
onSearch={(value) => {
setSearchKeyword(value);
<ScriptSearchField
t={t}
defaultValue={searchRequest}
onSearch={(req) => {
setSearchRequest(req);
}}
/>
</div>
Expand Down
112 changes: 32 additions & 80 deletions src/pages/options/routes/ScriptList/ScriptTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ import { i18nName } from "@App/locales/locales";
import { hashColor, ScriptIcons } from "../utils";
import type { ScriptLoading } from "@App/pages/store/features/script";
import { requestEnableScript, pinToTop, scriptClient, synchronizeClient } from "@App/pages/store/features/script";
import { type TFunction } from "i18next";
import { getCombinedMeta } from "@App/app/service/service_worker/utils";
import { parseTags } from "@App/app/repo/metadata";
import { EnableSwitch, HomeCell, MemoizedAvatar, SourceCell, UpdateTimeCell } from "./components";
import { EnableSwitch, HomeCell, MemoizedAvatar, ScriptSearchField, SourceCell, UpdateTimeCell } from "./components";
import type { SetSearchRequest } from "./hooks";
import type { SearchType } from "@App/app/service/service_worker/types";

type ListType = ScriptLoading;

Expand Down Expand Up @@ -115,47 +116,6 @@ const DraggableContainer = React.forwardRef<HTMLTableSectionElement, React.HTMLA

DraggableContainer.displayName = "DraggableContainer";

const FilterDropdown = React.memo(
({
filterKeys,
setFilterKeys,
confirm,
t,
inputRef,
}: {
filterKeys: string;
setFilterKeys: (filterKeys: string, callback?: (...args: any[]) => any) => void;
confirm: (...args: any[]) => any;
t: TFunction<"translation", undefined>;
inputRef: React.RefObject<RefInputType>;
}) => {
const { onSearchChange } = {
onSearchChange: (value: string) => {
setFilterKeys(value);
},
};
// onSearch 不能使用 useCallback / useMemo
const onSearch = () => {
confirm(filterKeys);
};
return (
<div className="arco-table-custom-filter flex flex-row gap-2">
<Input.Search
ref={inputRef}
size="small"
searchButton
style={{ minWidth: 240 }}
placeholder={t("enter_search_value", { search: `${t("name")}/${t("script_code")}` })}
defaultValue={filterKeys || ""}
onChange={onSearchChange}
onSearch={onSearch}
/>
</div>
);
}
);
FilterDropdown.displayName = "FilterDropdown";

function composeRefs<T>(...refs: React.Ref<T>[]): (node: T | null) => void {
return (node) => {
for (const ref of refs) {
Expand Down Expand Up @@ -460,7 +420,8 @@ 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;
setSearchKeyword: (keyword: string) => void;
searchRequest: { keyword: string; type: SearchType };
setSearchRequest: SetSearchRequest;
handleDelete: (item: ScriptLoading) => void;
handleConfig: (
item: ScriptLoading,
Expand All @@ -479,7 +440,8 @@ export const ScriptTable = ({
updateScripts,
setUserConfig,
setCloudScript,
setSearchKeyword,
searchRequest,
setSearchRequest,
handleDelete,
handleConfig,
handleRunStop,
Expand Down Expand Up @@ -532,18 +494,19 @@ export const ScriptTable = ({
dataIndex: "name",
sorter: (a, b) => a.name.localeCompare(b.name),
filterIcon: <IconSearch />,
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
filterDropdown: ({ confirm }: any) => {
return (
<FilterDropdown
filterKeys={filterKeys}
setFilterKeys={setFilterKeys}
confirm={(value) => {
setSearchKeyword(value || "");
confirm();
}}
t={t}
inputRef={inputRef}
/>
<div className="arco-table-custom-filter flex flex-row gap-2">
<ScriptSearchField
t={t}
defaultValue={searchRequest}
onSearch={(req) => {
setSearchRequest(req);
confirm();
}}
inputRef={inputRef}
/>
</div>
);
},
onFilterDropdownVisibleChange: (visible) => {
Expand Down Expand Up @@ -657,7 +620,8 @@ export const ScriptTable = ({
t,
sidebarOpen,
updateScripts,
setSearchKeyword,
searchRequest,
setSearchRequest,
navigate,
setSidebarOpen,
setViewMode,
Expand All @@ -679,30 +643,18 @@ export const ScriptTable = ({
useEffect(() => {
if (savedWidths === null) return;

setNewColumns((nColumns) => {
const widths = columns.map((item) => savedWidths[item.key!] ?? item.width);
const c = nColumns.length === widths.length ? nColumns : columns;
return c.map((item, i) => {
const width = widths[i];
let dest;
if (i === 8) {
// 第8列特殊处理,因为可能涉及到操作图的显示
dest = item.render === columns[i].render && item.title === columns[i].title ? item : columns[i];
} else {
dest = item;
// 主要只需要处理列宽变化的情况
setNewColumns(
columns.map((item, i) => {
if (savedWidths[item.key!] === undefined) {
return columns[i];
}
let m =
width === dest.width
? dest
: {
...dest,
width,
};
// 处理语言更新
if (m.title !== columns[i].title) m = { ...m, title: columns[i].title };
return m;
});
});
return {
...columns[i],
width: savedWidths[item.key!] ?? item.width,
};
})
);
}, [savedWidths, columns]);

useEffect(() => {
Expand Down
55 changes: 54 additions & 1 deletion src/pages/options/routes/ScriptList/components.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import type { SCRIPT_STATUS } from "@App/app/repo/scripts";
import { SCRIPT_STATUS_ENABLE } from "@App/app/repo/scripts";
import { scriptClient, type ScriptLoading } from "@App/pages/store/features/script";
import { Avatar, Message, Switch, Tag, Tooltip } from "@arco-design/web-react";
import { Avatar, Input, Message, Select, Space, Switch, Tag, Tooltip } from "@arco-design/web-react";
import React from "react";
import Text from "@arco-design/web-react/es/Typography/text";
import { TbWorldWww } from "react-icons/tb";
import { semTime } from "@App/pkg/utils/dayjs";
import { useTranslation } from "react-i18next";
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";

export const EnableSwitch = React.memo(
({
Expand Down Expand Up @@ -178,3 +181,53 @@ export const UpdateTimeCell = React.memo(({ className, script }: { className?: s
);
});
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>;
}

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;
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

React.memo 的比较函数存在问题。这里使用 === 比较 defaultValue 对象,但 searchRequest 是一个对象引用,即使内容相同,每次父组件重新渲染时传入的都是新的对象引用。这会导致 React.memo 失效,组件仍然会不必要地重新渲染。

建议改为深度比较:

(prevProps, nextProps) => {
  return (
    prevProps.t === nextProps.t &&
    prevProps.defaultValue.keyword === nextProps.defaultValue.keyword &&
    prevProps.defaultValue.type === nextProps.defaultValue.type
  );
}

或者在父组件中使用 useMemo 缓存 searchRequest 对象,但深度比较更可靠。

Suggested change
return prevProps.t === nextProps.t && prevProps.defaultValue === nextProps.defaultValue;
return (
prevProps.t === nextProps.t &&
prevProps.defaultValue.keyword === nextProps.defaultValue.keyword &&
prevProps.defaultValue.type === nextProps.defaultValue.type
);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

对象引用不会改变

function a(a, b) {
    console.log("对象相等", a === b);
}

let obj = { key: "123", value: "456" };

a(obj, obj);

a({ key: 123 }, { key: 123 })

}
);
ScriptSearchField.displayName = "ScriptSearchField";
Loading
Loading