Commit 5e0baeb2 authored by timel's avatar timel

feat: card detail content in card build

parent 2849c472
......@@ -118,6 +118,14 @@ export function isToken(typeCode: number): boolean {
return (typeCode & TYPE_TOKEN) > 0;
}
export function isMonster(typeCode: number): boolean {
return (typeCode & TYPE_MONSTER) > 0;
}
export function isLinkMonster(typeCode: number): boolean {
return (typeCode & TYPE_LINK) > 0;
}
// 属性
// const ATTRIBUTE_ALL = 0x7f; //
const ATTRIBUTE_EARTH = 0x01; //
......
......@@ -8,7 +8,7 @@
width: 100%;
height: 100%;
padding: 0 10px 20px 10px;
// transition: 0.2s;
transition: 0.2s;
}
.detail.open {
......@@ -21,6 +21,9 @@
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2), 0 0 30px 0 #ffffff54;
backdrop-filter: blur(20px);
@include utils.noise-bg;
padding: 15px;
display: flex;
flex-direction: column;
}
.btn-close {
......@@ -28,3 +31,27 @@
right: 5px;
top: 5px;
}
.card {
--width: 160px;
width: var(--width);
height: calc(var(--width) / var(--card-ratio));
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
box-shadow: 0px 14px 20px -5px rgba(0, 0, 0, 0.3);
}
.title {
font-size: 18px;
font-family: var(--theme-font);
margin: 20px 0 15px;
display: flex;
justify-content: space-between;
// color: rgba(255, 255, 255, 0.45);
}
.content {
// font-size: ;
color: white;
}
import { LineOutlined } from "@ant-design/icons";
import { Button } from "antd";
import { Avatar, Button, Descriptions } from "antd";
import classNames from "classnames";
import { useEffect, useMemo, useState } from "react";
import { type CardMeta, fetchCard, fetchStrings, Region } from "@/api";
import {
Attribute2StringCodeMap,
extraCardTypes,
isLinkMonster,
isMonster,
Race2StringCodeMap,
Type2StringCodeMap,
} from "@/common";
import { CardEffectText, IconFont, ScrollableArea, YgoCard } from "@/ui/Shared";
import styles from "./CardDetail.module.scss";
......@@ -9,16 +20,78 @@ export const CardDetail: React.FC<{
open: boolean;
onClose: () => void;
}> = ({ code, open, onClose }) => {
code;
const [card, setCard] = useState<CardMeta>();
useEffect(() => {
fetchCard(code).then(setCard);
}, [code]);
const cardType = useMemo(
() =>
extraCardTypes(card?.data.type ?? 0)
.map((t) => fetchStrings(Region.System, Type2StringCodeMap.get(t) || 0))
.join(" / "),
[card?.data.type]
);
return (
<div className={classNames(styles.detail, { [styles.open]: open })}>
<div className={styles.container}>
<Button
className={styles["btn-close"]}
icon={<LineOutlined />}
icon={<IconFont type="icon-side-bar-fill" size={16} />}
type="text"
onClick={onClose}
/>
<YgoCard className={styles.card} code={code} />
<div className={styles.title}>
<span>{card?.text.name}</span>
<Avatar size={22}></Avatar>
</div>
<ScrollableArea>
<Descriptions layout="vertical" size="small">
{card?.data.level && (
<Descriptions.Item label="等级">
{card?.data.level}
</Descriptions.Item>
)}
<Descriptions.Item label="类型" span={2}>
{cardType}
</Descriptions.Item>
{card?.data.attribute && (
<Descriptions.Item label="属性">
{fetchStrings(
Region.System,
Attribute2StringCodeMap.get(card?.data.attribute ?? 0) || 0
)}
</Descriptions.Item>
)}
{card?.data.race && (
<Descriptions.Item label="种族" span={2}>
{fetchStrings(
Region.System,
Race2StringCodeMap.get(card?.data.race ?? 0) || 0
)}
</Descriptions.Item>
)}
{isMonster(card?.data.type ?? 0) && (
<>
<Descriptions.Item label="攻击力">2000</Descriptions.Item>
{!isLinkMonster(card?.data.type ?? 0) && (
<Descriptions.Item label="守备力">0</Descriptions.Item>
)}
{card?.data.lscale && (
<Descriptions.Item label="灵摆刻度">
{card.data.lscale} - {card.data.rscale}
</Descriptions.Item>
)}
</>
)}
</Descriptions>
<Descriptions layout="vertical" size="small">
<Descriptions.Item label="卡片效果" span={3}>
<CardEffectText desc={card?.text.desc} />
</Descriptions.Item>
</Descriptions>
</ScrollableArea>
</div>
</div>
);
......
......@@ -23,11 +23,11 @@ import { memo, useCallback, useEffect, useRef, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { LoaderFunction } from "react-router-dom";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { type CardMeta, initForbiddens, searchCards } from "@/api";
import { isExtraDeckCard, isToken } from "@/common";
import { isToken } from "@/common";
import { FtsConditions } from "@/middleware/sqlite/fts";
import { deckStore, type IDeck, initStore } from "@/stores";
import {
......@@ -101,7 +101,7 @@ export const Component: React.FC = () => {
onAdd={() => console.log("add")}
/>
</ScrollableArea>
<CardDetail code={123} open={false} onClose={() => {}} />
<HigherCardDetail />
</div>
<div className={styles.content}>
<div className={styles.deck}>
......@@ -231,14 +231,14 @@ const Search: React.FC = () => {
[
["从新到旧", () => setSortRef((a, b) => b.id - a.id)],
["从旧到新", () => setSortRef((a, b) => a.id - b.id)],
["攻击力从高到低", genSort("atk")],
["攻击力从低到高", genSort("atk", -1)],
["守备力从高到低", genSort("def")],
["守备力从低到高", genSort("def", -1)],
["星/阶/刻/Link从高到低", genSort("level")],
["星/阶/刻/Link从低到高", genSort("level", -1)],
["灵摆刻度从高到低", genSort("lscale")],
["灵摆刻度从低到高", genSort("lscale", -1)],
["攻击力从高到低", 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 }));
......@@ -430,11 +430,6 @@ const SearchResults: React.FC<{
results: CardMeta[];
scrollToTop: () => void;
}> = memo(({ results, scrollToTop }) => {
const handleClick = (card: CardMeta) => {
const type = isExtraDeckCard(card.data.type ?? 0) ? "extra" : "main";
editDeckStore.canAdd(card, type).result && editDeckStore.add(type, card);
};
const itemsPerPage = 196; // 每页显示的数据数量
const [currentPage, setCurrentPage] = useState(1);
......@@ -450,12 +445,7 @@ const SearchResults: React.FC<{
<>
<div className={styles["search-cards"]}>
{currentData.map((card) => (
<Card
value={card}
key={card.id}
source="search"
onClick={() => handleClick(card)}
/>
<Card value={card} key={card.id} source="search" />
))}
</div>
{results.length > itemsPerPage && (
......@@ -482,9 +472,8 @@ const SearchResults: React.FC<{
const Card: React.FC<{
value: CardMeta;
source: Type | "search";
onClick?: () => void;
onRightClick?: () => void;
}> = memo(({ value, source, onClick, onRightClick }) => {
}> = memo(({ value, source, onRightClick }) => {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({
type: "Card",
......@@ -494,19 +483,43 @@ const Card: React.FC<{
}),
});
drag(ref);
const [showText, setShowText] = useState(true);
return (
<div
className={styles.card}
ref={ref}
style={{ opacity: isDragging && source !== "search" ? 0 : 1 }}
onClick={onClick}
onClick={() => {
selectedCard.id = value.id;
selectedCard.open = true;
}}
onContextMenu={(e) => {
e.preventDefault();
onRightClick?.();
}}
>
<div className={styles.cardname}>{value.text.name}</div>
<YgoCard className={styles.cardcover} code={value.id} />
{showText && <div className={styles.cardname}>{value.text.name}</div>}
<YgoCard
className={styles.cardcover}
code={value.id}
onLoad={() => setShowText(false)}
/>
</div>
);
});
const HigherCardDetail: React.FC = () => {
const { id, open } = useSnapshot(selectedCard);
return (
<CardDetail
open={open}
code={id}
onClose={() => (selectedCard.open = false)}
/>
);
};
const selectedCard = proxy({
id: 23995346,
open: false,
});
.desc {
line-height: 1.8;
font-size: 14px;
max-height: calc(100% - 237px);
font-family: var(--theme-font);
.maro-item {
display: flex;
margin-top: 8px;
gap: 6px;
}
}
import { Fragment, useMemo } from "react";
import styles from "./index.module.scss";
export const CardEffectText: React.FC<{ desc?: string }> = ({ desc = "" }) => {
if (!desc) return <></>;
const preprocessedDesc = useMemo(() => {
return addSpaces(desc);
}, [desc]);
return (
<div className={styles.desc}>
<RegexWrapper
text={preprocessedDesc}
re={/(①|②|③|④|⑤|⑥|⑦|⑧|⑨|⑩):.+?(?=((①|②|③|④|⑤|⑥|⑦|⑧|⑨|⑩):|$))/gs}
Wrapper={MaroListItem}
/>
</div>
);
};
/** 使用re去提取文本,并且将提取到的文本用Wrapper进行环绕 */
const RegexWrapper: React.FC<{
text: string;
re: RegExp;
Wrapper: React.FunctionComponent<any>;
}> = ({ text, re, Wrapper }) => {
const matches = text.match(re);
if (!matches) return <>{text}</>;
const sepRe = new RegExp(
matches?.reduce((acc, cur) => `${acc}|${cur}`) ?? ""
);
const parts = text.split(sepRe);
return (
<>
{parts.map((part, index) => (
<Fragment key={`${part}-${index}`}>
<div>{part}</div>
{index !== parts.length - 1 && <Wrapper>{matches?.[index]}</Wrapper>}
</Fragment>
))}
</>
);
};
const MaroListItem: React.FC<{ children: string }> = ({ children }) => {
return (
<div className={styles["maro-item"]}>
<span>{children[0]}</span>
<span>
<RegexWrapper
text={children.slice(2)}
re={/●.+?(?=(●|$))/gs}
Wrapper={CircleListItem}
/>
</span>
</div>
);
};
const CircleListItem: React.FC<{ children: string }> = ({ children }) => {
return children ? (
<div className={styles["maro-item"]}>
<span>{children[0]}</span>
<span>{children.slice(1)}</span>
</div>
) : (
<></>
);
};
function addSpaces(str: string): string {
const regex = /\d+/g;
return str.replace(regex, (match) => ` ${match} `);
}
// function removePendulumPrefix(str: string): string {}
......@@ -12,6 +12,7 @@ interface Props {
style?: CSSProperties;
width?: number;
onClick?: () => void;
onLoad?: () => void;
}
export const YgoCard: React.FC<Props> = (props) => {
......@@ -21,7 +22,8 @@ export const YgoCard: React.FC<Props> = (props) => {
isBack = false,
width,
style,
onClick = () => {},
onClick,
onLoad,
} = props;
return useMemo(
() => (
......@@ -30,6 +32,8 @@ export const YgoCard: React.FC<Props> = (props) => {
src={getCardImgUrl(code, isBack)}
style={{ width, ...style }}
onClick={onClick}
// 加载完成
onLoad={onLoad}
/>
),
[code]
......
export * from "./Background";
export * from "./CardEffectText";
export * from "./css";
export * from "./IconFont";
export * from "./Loading";
......
......@@ -30,7 +30,7 @@ export const theme: ThemeConfig = {
colorBgContainer: "hsla(0, 0%, 100%, 0.05)",
},
Dropdown: {
colorBgElevated: "#3f4d60",
colorBgElevated: "#2e3c50",
boxShadow:
"0 6px 16px 0 rgb(51 51 51 / 80%), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)",
},
......
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