Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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