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 { ...@@ -118,6 +118,14 @@ export function isToken(typeCode: number): boolean {
return (typeCode & TYPE_TOKEN) > 0; 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_ALL = 0x7f; //
const ATTRIBUTE_EARTH = 0x01; // const ATTRIBUTE_EARTH = 0x01; //
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0 10px 20px 10px; padding: 0 10px 20px 10px;
// transition: 0.2s; transition: 0.2s;
} }
.detail.open { .detail.open {
...@@ -21,6 +21,9 @@ ...@@ -21,6 +21,9 @@
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2), 0 0 30px 0 #ffffff54; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2), 0 0 30px 0 #ffffff54;
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
@include utils.noise-bg; @include utils.noise-bg;
padding: 15px;
display: flex;
flex-direction: column;
} }
.btn-close { .btn-close {
...@@ -28,3 +31,27 @@ ...@@ -28,3 +31,27 @@
right: 5px; right: 5px;
top: 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 { Avatar, Button, Descriptions } from "antd";
import { Button } from "antd";
import classNames from "classnames"; 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"; import styles from "./CardDetail.module.scss";
...@@ -9,16 +20,78 @@ export const CardDetail: React.FC<{ ...@@ -9,16 +20,78 @@ export const CardDetail: React.FC<{
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
}> = ({ code, open, onClose }) => { }> = ({ 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 ( return (
<div className={classNames(styles.detail, { [styles.open]: open })}> <div className={classNames(styles.detail, { [styles.open]: open })}>
<div className={styles.container}> <div className={styles.container}>
<Button <Button
className={styles["btn-close"]} className={styles["btn-close"]}
icon={<LineOutlined />} icon={<IconFont type="icon-side-bar-fill" size={16} />}
type="text" type="text"
onClick={onClose} 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>
</div> </div>
); );
......
...@@ -23,11 +23,11 @@ import { memo, useCallback, useEffect, useRef, useState } from "react"; ...@@ -23,11 +23,11 @@ import { memo, useCallback, useEffect, useRef, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd"; import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import { LoaderFunction } from "react-router-dom"; import { LoaderFunction } from "react-router-dom";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils"; import { subscribeKey } from "valtio/utils";
import { type CardMeta, initForbiddens, searchCards } from "@/api"; import { type CardMeta, initForbiddens, searchCards } from "@/api";
import { isExtraDeckCard, isToken } from "@/common"; import { isToken } from "@/common";
import { FtsConditions } from "@/middleware/sqlite/fts"; import { FtsConditions } from "@/middleware/sqlite/fts";
import { deckStore, type IDeck, initStore } from "@/stores"; import { deckStore, type IDeck, initStore } from "@/stores";
import { import {
...@@ -101,7 +101,7 @@ export const Component: React.FC = () => { ...@@ -101,7 +101,7 @@ export const Component: React.FC = () => {
onAdd={() => console.log("add")} onAdd={() => console.log("add")}
/> />
</ScrollableArea> </ScrollableArea>
<CardDetail code={123} open={false} onClose={() => {}} /> <HigherCardDetail />
</div> </div>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.deck}> <div className={styles.deck}>
...@@ -231,14 +231,14 @@ const Search: React.FC = () => { ...@@ -231,14 +231,14 @@ const Search: React.FC = () => {
[ [
["从新到旧", () => setSortRef((a, b) => b.id - a.id)], ["从新到旧", () => setSortRef((a, b) => b.id - a.id)],
["从旧到新", () => setSortRef((a, b) => a.id - b.id)], ["从旧到新", () => setSortRef((a, b) => a.id - b.id)],
["攻击力从高到低", genSort("atk")], ["攻击力从高到低", genSort("atk", -1)],
["攻击力从低到高", genSort("atk", -1)], ["攻击力从低到高", genSort("atk")],
["守备力从高到低", genSort("def")], ["守备力从高到低", genSort("def", -1)],
["守备力从低到高", genSort("def", -1)], ["守备力从低到高", genSort("def")],
["星/阶/刻/Link从高到低", genSort("level")], ["星/阶/刻/Link从高到低", genSort("level", -1)],
["星/阶/刻/Link从低到高", genSort("level", -1)], ["星/阶/刻/Link从低到高", genSort("level")],
["灵摆刻度从高到低", genSort("lscale")], ["灵摆刻度从高到低", genSort("lscale", -1)],
["灵摆刻度从低到高", genSort("lscale", -1)], ["灵摆刻度从低到高", genSort("lscale")],
] as const ] as const
).map(([label, onClick], key) => ({ key, label, onClick })); ).map(([label, onClick], key) => ({ key, label, onClick }));
...@@ -430,11 +430,6 @@ const SearchResults: React.FC<{ ...@@ -430,11 +430,6 @@ const SearchResults: React.FC<{
results: CardMeta[]; results: CardMeta[];
scrollToTop: () => void; scrollToTop: () => void;
}> = memo(({ results, scrollToTop }) => { }> = 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 itemsPerPage = 196; // 每页显示的数据数量
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
...@@ -450,12 +445,7 @@ const SearchResults: React.FC<{ ...@@ -450,12 +445,7 @@ const SearchResults: React.FC<{
<> <>
<div className={styles["search-cards"]}> <div className={styles["search-cards"]}>
{currentData.map((card) => ( {currentData.map((card) => (
<Card <Card value={card} key={card.id} source="search" />
value={card}
key={card.id}
source="search"
onClick={() => handleClick(card)}
/>
))} ))}
</div> </div>
{results.length > itemsPerPage && ( {results.length > itemsPerPage && (
...@@ -482,9 +472,8 @@ const SearchResults: React.FC<{ ...@@ -482,9 +472,8 @@ const SearchResults: React.FC<{
const Card: React.FC<{ const Card: React.FC<{
value: CardMeta; value: CardMeta;
source: Type | "search"; source: Type | "search";
onClick?: () => void;
onRightClick?: () => void; onRightClick?: () => void;
}> = memo(({ value, source, onClick, onRightClick }) => { }> = memo(({ value, source, onRightClick }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({ const [{ isDragging }, drag] = useDrag({
type: "Card", type: "Card",
...@@ -494,19 +483,43 @@ const Card: React.FC<{ ...@@ -494,19 +483,43 @@ const Card: React.FC<{
}), }),
}); });
drag(ref); drag(ref);
const [showText, setShowText] = useState(true);
return ( return (
<div <div
className={styles.card} className={styles.card}
ref={ref} ref={ref}
style={{ opacity: isDragging && source !== "search" ? 0 : 1 }} style={{ opacity: isDragging && source !== "search" ? 0 : 1 }}
onClick={onClick} onClick={() => {
selectedCard.id = value.id;
selectedCard.open = true;
}}
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
onRightClick?.(); onRightClick?.();
}} }}
> >
<div className={styles.cardname}>{value.text.name}</div> {showText && <div className={styles.cardname}>{value.text.name}</div>}
<YgoCard className={styles.cardcover} code={value.id} /> <YgoCard
className={styles.cardcover}
code={value.id}
onLoad={() => setShowText(false)}
/>
</div> </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 { ...@@ -12,6 +12,7 @@ interface Props {
style?: CSSProperties; style?: CSSProperties;
width?: number; width?: number;
onClick?: () => void; onClick?: () => void;
onLoad?: () => void;
} }
export const YgoCard: React.FC<Props> = (props) => { export const YgoCard: React.FC<Props> = (props) => {
...@@ -21,7 +22,8 @@ export const YgoCard: React.FC<Props> = (props) => { ...@@ -21,7 +22,8 @@ export const YgoCard: React.FC<Props> = (props) => {
isBack = false, isBack = false,
width, width,
style, style,
onClick = () => {}, onClick,
onLoad,
} = props; } = props;
return useMemo( return useMemo(
() => ( () => (
...@@ -30,6 +32,8 @@ export const YgoCard: React.FC<Props> = (props) => { ...@@ -30,6 +32,8 @@ export const YgoCard: React.FC<Props> = (props) => {
src={getCardImgUrl(code, isBack)} src={getCardImgUrl(code, isBack)}
style={{ width, ...style }} style={{ width, ...style }}
onClick={onClick} onClick={onClick}
// 加载完成
onLoad={onLoad}
/> />
), ),
[code] [code]
......
export * from "./Background"; export * from "./Background";
export * from "./CardEffectText";
export * from "./css"; export * from "./css";
export * from "./IconFont"; export * from "./IconFont";
export * from "./Loading"; export * from "./Loading";
......
...@@ -30,7 +30,7 @@ export const theme: ThemeConfig = { ...@@ -30,7 +30,7 @@ export const theme: ThemeConfig = {
colorBgContainer: "hsla(0, 0%, 100%, 0.05)", colorBgContainer: "hsla(0, 0%, 100%, 0.05)",
}, },
Dropdown: { Dropdown: {
colorBgElevated: "#3f4d60", colorBgElevated: "#2e3c50",
boxShadow: 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)", "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