Commit 41c6c3c2 authored by Chunchi Che's avatar Chunchi Che

move DeckZone and DeckCard to Shared

parent fed1294e
Pipeline #23296 failed with stages
in 15 minutes and 48 seconds
......@@ -57,33 +57,7 @@
display: flex;
flex-direction: column;
height: 100%;
}
.main,
.extra,
.side {
transition: 0.2s;
position: relative;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 0.75rem;
&.over {
background-color: hsla(0, 0%, 100%, 0.05);
}
&.not-allow-to-drop {
background-color: rgba(255, 0, 0, 0.15);
cursor: not-allowed;
}
}
.main {
flex: 3;
}
.extra,
.side {
flex: 1;
}
.card-continer {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 5px;
--card-grid: 10;
}
background-color: hsla(0, 0%, 100%, 0.05);
backdrop-filter: blur(5px);
......@@ -98,45 +72,6 @@
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.card {
cursor: move;
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
aspect-ratio: var(--card-ratio);
position: relative;
background-size: contain;
content-visibility: auto;
transition: 0.1s;
&:hover {
filter: brightness(0.9);
}
.cardname {
font-size: 12px;
position: absolute;
padding: 5px;
top: 0;
bottom: 0;
max-height: 100%;
margin: auto;
left: 0;
height: fit-content;
width: 100%;
text-align: center;
line-height: 1.75em;
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
}
.cardcover {
position: relative;
}
.cardlimit {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
}
}
.search-cards-container {
height: 100%;
.search-cards {
......
......@@ -16,28 +16,28 @@ import {
Pagination,
Space,
} from "antd";
import classNames from "classnames";
import { isEqual } from "lodash-es";
import { type OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { DndProvider, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { LoaderFunction } from "react-router-dom";
import { proxy, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { type CardMeta, forbidden, searchCards } from "@/api";
import { type CardMeta, searchCards } from "@/api";
import { isToken } from "@/common";
import { useConfig } from "@/config";
import { FtsConditions } from "@/middleware/sqlite/fts";
import { deckStore, type IDeck, initStore } from "@/stores";
import {
Background,
DeckCard,
DeckZone,
IconFont,
Loading,
ScrollableArea,
YgoCard,
} from "@/ui/Shared";
import { Type } from "@/ui/Shared/DeckZone";
import { CardDetail } from "./CardDetail";
import { DeckSelect } from "./DeckSelect";
......@@ -49,11 +49,8 @@ import {
downloadDeckAsYDK,
editingDeckToIDeck,
iDeckToEditingDeck,
type Type,
} from "./utils";
const { assetsPath } = useConfig();
export const loader: LoaderFunction = async () => {
// 必须先加载卡组,不然页面会崩溃
if (!initStore.decks) {
......@@ -410,84 +407,6 @@ const Search: React.FC = () => {
);
};
/** 正在组卡的zone,包括main/extra/side */
const DeckZone: React.FC<{
type: Type;
cards: CardMeta[];
canAdd: (
card: CardMeta,
type: Type,
source: Type | "search",
) => { result: boolean; reason: string };
onChange: (
card: CardMeta,
source: Type | "search",
destination: Type,
) => void;
onElementClick: (card: CardMeta) => void;
onElementRightClick: (card: CardMeta) => void;
}> = ({
type,
cards,
canAdd,
onChange,
onElementClick,
onElementRightClick,
}) => {
const { message } = App.useApp();
const [allowToDrop, setAllowToDrop] = useState(false);
const [{ isOver }, dropRef] = useDrop({
accept: ["Card"], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type
// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: ({ value, source }: { value: CardMeta; source: Type | "search" }) => {
if (type === source) return;
const { result, reason } = canAdd(value, type, source);
if (result) {
onChange(value, source, type);
} else {
message.error(reason);
}
},
hover: ({ value, source }) => {
setAllowToDrop(
type !== source ? canAdd(value, type, source).result : true,
);
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
});
return (
<div
className={classNames(styles[type], {
[styles.over]: isOver,
[styles["not-allow-to-drop"]]: isOver && !allowToDrop,
})}
ref={dropRef}
>
<div className={styles["card-continer"]}>
{cards.map((card, i) => (
<DeckCard
value={card}
key={card.id + i + type}
source={type}
onClick={() => {
onElementClick(card);
}}
onRightClick={() => {
onElementRightClick(card);
}}
/>
))}
<div className={styles["editing-zone-name"]}>
{`${type.toUpperCase()}: ${cards.length}`}
</div>
</div>
</div>
);
};
/** 搜索区的搜索结果,使用memo避免重复渲染 */
const SearchResults: React.FC<{
results: CardMeta[];
......@@ -539,51 +458,6 @@ const SearchResults: React.FC<{
);
});
/** 本组件内使用的单张卡片,增加了文字在图片下方 */
export const DeckCard: React.FC<{
value: CardMeta;
source: Type | "search";
onRightClick?: () => void;
onClick?: () => void;
}> = memo(({ value, source, onRightClick, onClick }) => {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({
type: "Card",
item: { value, source },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drag(ref);
const [showText, setShowText] = useState(true);
const limitCnt = forbidden.get(value.id);
return (
<div
className={styles.card}
ref={ref}
style={{ opacity: isDragging && source !== "search" ? 0 : 1 }}
onClick={onClick}
onContextMenu={(e) => {
e.preventDefault();
onRightClick?.();
}}
>
{showText && <div className={styles.cardname}>{value.text.name}</div>}
<YgoCard
className={styles.cardcover}
code={value.id}
onLoad={() => setShowText(false)}
/>
{limitCnt !== undefined && (
<img
className={styles.cardlimit}
src={`${assetsPath}/Limit0${limitCnt}.png`}
/>
)}
</div>
);
});
const HigherCardDetail: React.FC = () => {
const { id, open } = useSnapshot(selectedCard);
return (
......
......@@ -2,8 +2,9 @@ import { proxy } from "valtio";
import { type CardMeta } from "@/api";
import { isExtraDeckCard, isToken } from "@/common";
import { Type } from "@/ui/Shared/DeckZone";
import { compareCards, type EditingDeck, type Type } from "./utils";
import { compareCards, type EditingDeck } from "./utils";
export const editDeckStore = proxy({
deckName: "",
......
......@@ -2,8 +2,6 @@ import { type CardMeta, fetchCard } from "@/api";
import { tellCardBasicType, tellCardSecondaryType } from "@/common";
import { type IDeck } from "@/stores";
export type Type = "main" | "extra" | "side";
/** 用在卡组编辑 */
export interface EditingDeck {
deckName: string;
......
......@@ -41,6 +41,7 @@
display: flex;
flex-direction: column;
height: 100%;
--card-grid: 15;
}
background-color: hsla(0, 0%, 100%, 0.05);
backdrop-filter: blur(5px);
......
import { CheckOutlined, UndoOutlined } from "@ant-design/icons";
import { Button, Space } from "antd";
import React from "react";
import React, { useState } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { fetchCard } from "@/api";
import { deckStore, IDeck } from "@/stores";
import { CardDetail } from "../BuildDeck/CardDetail";
import { Background, ScrollableArea } from "../Shared";
import { Background, DeckZone, ScrollableArea } from "../Shared";
import { Chat } from "../WaitRoom/Chat";
import styles from "./index.module.scss";
export const Component: React.FC = () => {
const [deck, setDeck] = useState<IDeck>({ ...deckStore.decks[0] });
return (
<DndProvider backend={HTML5Backend}>
<Background />
......@@ -30,7 +34,20 @@ export const Component: React.FC = () => {
</Button>
</Space>
</Space>
<ScrollableArea className={styles["deck-zone"]}></ScrollableArea>
<ScrollableArea className={styles["deck-zone"]}>
{(["main", "extra", "side"] as const).map((type) => (
<DeckZone
key={type}
type={type}
cards={[...deck[type]].map((id) => fetchCard(id))}
canAdd={(card, type, source) => {
return { result: true, reason: "" };
}}
onChange={(card, source, destination) => {}}
onElementClick={(card) => {}}
/>
))}
</ScrollableArea>
</div>
</div>
<div className={styles["detail-container"]}>
......
.card {
cursor: move;
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
aspect-ratio: var(--card-ratio);
position: relative;
background-size: contain;
content-visibility: auto;
transition: 0.1s;
&:hover {
filter: brightness(0.9);
}
.cardname {
font-size: 12px;
position: absolute;
padding: 5px;
top: 0;
bottom: 0;
max-height: 100%;
margin: auto;
left: 0;
height: fit-content;
width: 100%;
text-align: center;
line-height: 1.75em;
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
}
.cardcover {
position: relative;
}
.cardlimit {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
}
}
import React, { memo, useRef, useState } from "react";
import { useDrag } from "react-dnd";
import { CardMeta, forbidden } from "@/api";
import { useConfig } from "@/config";
import { Type } from "../DeckZone";
import { YgoCard } from "../YgoCard";
import styles from "./index.module.scss";
const { assetsPath } = useConfig();
/** 组卡页和Side页使用的单张卡片,增加了文字和禁限数量 */
export const DeckCard: React.FC<{
value: CardMeta;
source: Type | "search";
onRightClick?: () => void;
onClick?: () => void;
}> = memo(({ value, source, onRightClick, onClick }) => {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({
type: "Card",
item: { value, source },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drag(ref);
const [showText, setShowText] = useState(true);
const limitCnt = forbidden.get(value.id);
return (
<div
className={styles.card}
ref={ref}
style={{ opacity: isDragging && source !== "search" ? 0 : 1 }}
onClick={onClick}
onContextMenu={(e) => {
e.preventDefault();
onRightClick?.();
}}
>
{showText && <div className={styles.cardname}>{value.text.name}</div>}
<YgoCard
className={styles.cardcover}
code={value.id}
onLoad={() => setShowText(false)}
/>
{limitCnt !== undefined && (
<img
className={styles.cardlimit}
src={`${assetsPath}/Limit0${limitCnt}.png`}
/>
)}
</div>
);
});
.main,
.extra,
.side {
transition: 0.2s;
position: relative;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 0.75rem;
&.over {
background-color: hsla(0, 0%, 100%, 0.05);
}
&.not-allow-to-drop {
background-color: rgba(255, 0, 0, 0.15);
cursor: not-allowed;
}
}
.main {
flex: 3;
}
.extra,
.side {
flex: 1;
}
.card-continer {
display: grid;
grid-template-columns: repeat(var(--card-grid), 1fr);
gap: 5px;
}
.editing-zone-name {
position: absolute;
right: 0;
bottom: 0;
background-color: #212332;
color: hsla(0, 0%, 100%, 0.3);
font-size: 12px;
padding: 2px 6px;
font-family: var(--theme-font);
user-select: none;
}
import { App } from "antd";
import classNames from "classnames";
import React, { useState } from "react";
import { useDrop } from "react-dnd";
import { CardMeta } from "@/api";
import { DeckCard } from "../DeckCard";
import styles from "./index.module.scss";
/** 正在组卡的zone,包括main/extra/side
* 该组件内部没有引用任何store,是解耦的*/
export type Type = "main" | "extra" | "side";
export const DeckZone: React.FC<{
type: Type;
cards: CardMeta[];
canAdd: (
card: CardMeta,
type: Type,
source: Type | "search",
) => { result: boolean; reason: string };
onChange: (
card: CardMeta,
source: Type | "search",
destination: Type,
) => void;
onElementClick: (card: CardMeta) => void;
onElementRightClick?: (card: CardMeta) => void;
}> = ({
type,
cards,
canAdd,
onChange,
onElementClick,
onElementRightClick,
}) => {
const { message } = App.useApp();
const [allowToDrop, setAllowToDrop] = useState(false);
const [{ isOver }, dropRef] = useDrop({
accept: ["Card"], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type
// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: ({ value, source }: { value: CardMeta; source: Type | "search" }) => {
if (type === source) return;
const { result, reason } = canAdd(value, type, source);
if (result) {
onChange(value, source, type);
} else {
message.error(reason);
}
},
hover: ({ value, source }) => {
setAllowToDrop(
type !== source ? canAdd(value, type, source).result : true,
);
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
});
return (
<div
className={classNames(styles[type], {
[styles.over]: isOver,
[styles["not-allow-to-drop"]]: isOver && !allowToDrop,
})}
ref={dropRef}
>
<div className={styles["card-continer"]}>
{cards.map((card, i) => (
<DeckCard
value={card}
key={card.id + i + type}
source={type}
onClick={() => {
onElementClick(card);
}}
onRightClick={() => {
onElementRightClick?.(card);
}}
/>
))}
<div className={styles["editing-zone-name"]}>
{`${type.toUpperCase()}: ${cards.length}`}
</div>
</div>
</div>
);
};
export * from "./Background";
export * from "./CardEffectText";
export * from "./css";
export * from "./DeckCard";
export * from "./DeckZone";
export * from "./IconFont";
export * from "./Loading";
export * from "./Scrollbar";
......
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