Commit 8dc296e4 authored by Chunchi Che's avatar Chunchi Che

rerange Search component

parent 35b866ed
Pipeline #27604 passed with stages
in 7 minutes and 7 seconds
import {
DeleteOutlined,
FilterOutlined,
SearchOutlined,
SortAscendingOutlined,
} from "@ant-design/icons";
import { App, Button, Dropdown, Input, message, Pagination } from "antd";
import { MenuProps } from "antd/lib";
import { isEqual } from "lodash-es";
import { OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import { useDrop } from "react-dnd";
import { CardMeta, searchCards } from "@/api";
import { isExtraDeckCard, isToken } from "@/common";
import { emptySearchConditions, FtsConditions } from "@/middleware/sqlite/fts";
import {
DeckCard,
DeckCardMouseUpEvent,
IconFont,
ScrollableArea,
Type,
} from "@/ui/Shared";
import { selectedCard } from "..";
import { Filter } from "../Filter";
import styles from "../index.module.scss";
import { editDeckStore } from "../store";
/** 卡片库,选择卡片加入正在编辑的卡组 */
export const CardDatabase: React.FC = () => {
const { modal } = App.useApp();
const [searchWord, setSearchWord] = useState("");
const [searchConditions, setSearchConditions] = useState<FtsConditions>(
emptySearchConditions,
);
const [searchResult, setSearchResult] = useState<CardMeta[]>([]);
const defaultSort = (a: CardMeta, b: CardMeta) => a.id - b.id;
const sortRef = useRef<(a: CardMeta, b: CardMeta) => number>(defaultSort);
const [sortEdited, setSortEdited] = useState(false);
const setSortRef = (sort: (a: CardMeta, b: CardMeta) => number) => {
sortRef.current = sort;
setSearchResult([...searchResult.sort(sortRef.current)]);
setSortEdited(true);
};
const genSort = (key: keyof CardMeta["data"], scale: 1 | -1 = 1) => {
return () =>
setSortRef(
(a: CardMeta, b: CardMeta) =>
((a.data?.[key] ?? 0) - (b.data?.[key] ?? 0)) * scale,
);
};
const dropdownOptions: MenuProps["items"] = (
[
["从新到旧", () => setSortRef((a, b) => b.id - a.id)],
["从旧到新", () => setSortRef((a, b) => a.id - b.id)],
["攻击力从高到低", genSort("atk", -1)],
["攻击力从低到高", genSort("atk")],
["守备力从高到低", genSort("def", -1)],
["守备力从低到高", genSort("def")],
["星/阶/刻/Link从高到低", genSort("level", -1)],
["星/阶/刻/Link从低到高", genSort("level")],
["灵摆刻度从高到低", genSort("lscale", -1)],
["灵摆刻度从低到高", genSort("lscale")],
] as const
).map(([label, onClick], key) => ({ key, label, onClick }));
const handleSearch = (conditions: FtsConditions = searchConditions) => {
const result = searchCards({ query: searchWord, conditions })
.filter((card) => !isToken(card.data.type ?? 0))
.sort(sortRef.current); // 衍生物不显示
setSearchResult(() => result);
};
useEffect(() => {
handleSearch();
}, []);
const [_, dropRef] = useDrop({
accept: ["Card"], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type
// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: ({ value, source }: { value: CardMeta; source: Type | "search" }) => {
if (source !== "search") {
editDeckStore.remove(source, value);
}
},
});
const showFilterModal = () => {
const { destroy } = modal.info({
width: 500,
centered: true,
title: null,
icon: null,
content: (
<Filter
conditions={searchConditions}
onConfirm={(newConditions) => {
setSearchConditions(newConditions);
destroy();
setTimeout(() => handleSearch(newConditions), 200); // 先收起再搜索
}}
onCancel={() => destroy()}
/>
),
footer: null,
});
};
/** 滚动条的ref,用来在翻页之后快速回顶 */
const ref = useRef<OverlayScrollbarsComponentRef<"div">>(null);
const scrollToTop = useCallback(() => {
const viewport = ref.current?.osInstance()?.elements().viewport;
if (viewport) viewport.scrollTop = 0;
}, []);
return (
<div className={styles.container} ref={dropRef}>
<div className={styles.title}>
<Input
placeholder="关键词(空格分隔)"
bordered={false}
suffix={
<Button
type="text"
icon={<SearchOutlined />}
onClick={() => handleSearch()}
/>
}
value={searchWord}
onChange={(e) => setSearchWord(e.target.value)}
onKeyUp={(e) => e.key === "Enter" && handleSearch()}
allowClear
/>
</div>
<div className={styles["select-btns"]}>
<Button
block
type={
isEqual(emptySearchConditions, searchConditions)
? "text"
: "primary"
}
icon={<FilterOutlined />}
onClick={showFilterModal}
>
筛选
</Button>
<Dropdown
menu={{ items: dropdownOptions }}
trigger={["click"]}
placement="bottom"
arrow
>
<Button
block
type={sortEdited ? "primary" : "text"}
icon={<SortAscendingOutlined />}
>
<span>
排列
<span className={styles["search-count"]}>
({searchResult.length})
</span>
</span>
</Button>
</Dropdown>
<Button
block
type="text"
icon={<DeleteOutlined />}
onClick={() => {
setSearchConditions(emptySearchConditions);
setSortRef(defaultSort);
setSortEdited(false);
handleSearch(emptySearchConditions);
}}
>
重置
</Button>
</div>
<ScrollableArea className={styles["search-cards-container"]} ref={ref}>
{searchResult.length ? (
<SearchResults results={searchResult} scrollToTop={scrollToTop} />
) : (
<div className={styles.empty}>
<IconFont type="icon-empty" size={40} />
<div>无搜索结果</div>
</div>
)}
</ScrollableArea>
</div>
);
};
/** 搜索区的搜索结果,使用memo避免重复渲染 */
const SearchResults: React.FC<{
results: CardMeta[];
scrollToTop: () => void;
}> = memo(({ results, scrollToTop }) => {
const itemsPerPage = 196; // 每页显示的数据数量
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
setCurrentPage(1);
}, [results]);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentData = results.slice(startIndex, endIndex);
const showSelectedCard = (card: CardMeta) => {
selectedCard.id = card.id;
selectedCard.open = true;
};
const handleAddCardToMain = (card: CardMeta) => {
const cardType = card.data.type ?? 0;
const isExtraCard = isExtraDeckCard(cardType);
const type = isExtraCard ? "extra" : "main";
const { result, reason } = editDeckStore.canAdd(card, type, "search");
if (result) {
editDeckStore.add(type, card);
} else {
message.error(reason);
}
};
const handleAddCardToSide = (card: CardMeta) => {
const { result, reason } = editDeckStore.canAdd(card, "side", "search");
if (result) {
editDeckStore.add("side", card);
} else {
message.error(reason);
}
};
/** safari 不支持 onAuxClick,所以使用 mousedown 模拟 */
const handleMouseUp = (payload: DeckCardMouseUpEvent) => {
const { event, card } = payload;
switch (event.button) {
// 左键
case 0:
showSelectedCard(card);
break;
// 中键
case 1:
handleAddCardToSide(card);
break;
// 右键
case 2:
handleAddCardToMain(card);
break;
default:
break;
}
};
return (
<>
<div className={styles["search-cards"]}>
{currentData.map((card) => (
<DeckCard
value={card}
key={card.id}
source="search"
onMouseUp={handleMouseUp}
onMouseEnter={() => showSelectedCard(card)}
/>
))}
</div>
{results.length > itemsPerPage && (
<div style={{ textAlign: "center", padding: "10px 0 20px" }}>
<Pagination
current={currentPage}
onChange={(page) => {
setCurrentPage(page);
scrollToTop();
}}
total={results.length}
pageSize={itemsPerPage}
showSizeChanger={false}
showLessItems
hideOnSinglePage
/>
</div>
)}
</>
);
});
This diff is collapsed.
......@@ -315,11 +315,7 @@ export const Component: React.FC = () => {
icon={<IconFont type="icon-record" size={24} />}
onClick={replayOpen}
/>
<Mode
title={i18n("WIPTitle")}
desc={i18n("WIPDesc")}
icon={null}
/>
<Mode title={i18n("WIPTitle")} desc={i18n("WIPDesc")} icon={null} />
</div>
</div>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment