Commit 93a8d403 authored by timel's avatar timel

feat: basic drag and drop in deck build

parent df7a8c3b
Pipeline #23014 failed with stages
in 16 minutes and 11 seconds
......@@ -30,6 +30,8 @@
"overlayscrollbars-react": "^0.5.1",
"react": "^18.2.0",
"react-animated-numbers": "^0.16.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-router-dom": "^6.10.0",
......@@ -3100,6 +3102,21 @@
"react-dom": ">=16.9.0"
}
},
"node_modules/@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
},
"node_modules/@react-dnd/invariant": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
},
"node_modules/@react-dnd/shallowequal": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
},
"node_modules/@react-spring/animated": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.2.tgz",
......@@ -7924,6 +7941,16 @@
"node": ">=8"
}
},
"node_modules/dnd-core": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
"dependencies": {
"@react-dnd/asap": "^5.0.1",
"@react-dnd/invariant": "^4.0.1",
"redux": "^4.2.0"
}
},
"node_modules/dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
......@@ -10930,6 +10957,14 @@
"node": ">=4.0.0"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/home-or-tmp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
......@@ -21292,6 +21327,43 @@
"which": "bin/which"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
"dependencies": {
"@react-dnd/invariant": "^4.0.1",
"@react-dnd/shallowequal": "^4.0.1",
"dnd-core": "^16.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"@types/hoist-non-react-statics": ">= 3.3.1",
"@types/node": ">= 12",
"@types/react": ">= 16",
"react": ">= 16.14"
},
"peerDependenciesMeta": {
"@types/hoist-non-react-statics": {
"optional": true
},
"@types/node": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dnd-html5-backend": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
"dependencies": {
"dnd-core": "^16.0.1"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
......@@ -22953,6 +23025,14 @@
"node": ">=8"
}
},
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
......@@ -30753,6 +30833,21 @@
"rc-util": "^5.29.2"
}
},
"@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
},
"@react-dnd/invariant": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
},
"@react-dnd/shallowequal": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
},
"@react-spring/animated": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.2.tgz",
......@@ -34543,6 +34638,16 @@
"path-type": "^4.0.0"
}
},
"dnd-core": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
"requires": {
"@react-dnd/asap": "^5.0.1",
"@react-dnd/invariant": "^4.0.1",
"redux": "^4.2.0"
}
},
"dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
......@@ -36844,6 +36949,14 @@
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"home-or-tmp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
......@@ -44870,6 +44983,26 @@
}
}
},
"react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
"requires": {
"@react-dnd/invariant": "^4.0.1",
"@react-dnd/shallowequal": "^4.0.1",
"dnd-core": "^16.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
}
},
"react-dnd-html5-backend": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
"requires": {
"dnd-core": "^16.0.1"
}
},
"react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
......@@ -46148,6 +46281,14 @@
"strip-indent": "^3.0.0"
}
},
"redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"requires": {
"@babel/runtime": "^7.9.2"
}
},
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
......@@ -88,6 +88,7 @@
}
.card {
cursor: move;
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
aspect-ratio: var(--card-ratio);
......
......@@ -15,10 +15,12 @@ import {
Space,
type ThemeConfig,
} from "antd";
import { memo, useEffect, useState } from "react";
import { memo, 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 { v4 as v4uuid } from "uuid";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { type CardMeta, searchCards } from "@/api";
......@@ -64,6 +66,7 @@ export const Component: React.FC = () => {
const [selectedDeck, setSelectedDeck] = useState<IDeck>(deckStore.decks[0]);
return (
<DndProvider backend={HTML5Backend}>
<ConfigProvider theme={theme}>
<Background />
<div className={styles.layout} style={{ width: "100%" }}>
......@@ -98,6 +101,7 @@ export const Component: React.FC = () => {
</div>
</div>
</ConfigProvider>
</DndProvider>
);
};
Component.displayName = "Build";
......@@ -107,14 +111,9 @@ const DeckEditor: React.FC<{
deck: IDeck;
onSave: (deck: IDeck) => void;
}> = ({ deck, onSave }) => {
const [editingDeck, setEditingDeck] = useState<EditingDeck>({
deckName: deck.deckName,
main: [],
extra: [],
side: [],
});
const snapEditDeck = useSnapshot(editDeckStore);
useEffect(() => {
iDeckToEditingDeck(deck).then(setEditingDeck);
iDeckToEditingDeck(deck).then(editDeckStore.set);
}, [deck]);
return (
<div className={styles.container}>
......@@ -125,9 +124,12 @@ const DeckEditor: React.FC<{
prefix={<EditOutlined />}
style={{ width: 400 }}
onChange={(e) =>
setEditingDeck({ ...editingDeck, deckName: e.target.value })
editDeckStore.set({
...editDeckStore,
deckName: e.target.value,
})
}
value={editingDeck.deckName}
value={snapEditDeck.deckName}
/>
<Space style={{ marginRight: 6 }}>
<Button type="text" size="small" icon={<DeleteOutlined />}>
......@@ -140,7 +142,7 @@ const DeckEditor: React.FC<{
type="text"
size="small"
icon={<CheckOutlined />}
onClick={() => onSave(editingDeckToIDeck(editingDeck))}
onClick={() => onSave(editingDeckToIDeck(editDeckStore))}
>
保存
</Button>
......@@ -148,13 +150,7 @@ const DeckEditor: React.FC<{
</Space>
<ScrollableArea className={styles["deck-zone"]}>
{(["main", "extra", "side"] as const).map((type) => (
<div key={type} className={styles[type]}>
<div className={styles["card-continer"]}>
{editingDeck[type].map((item) => (
<Card value={item} key={v4uuid()} />
))}
</div>
</div>
<DeckZone key={type} type={type} />
))}
</ScrollableArea>
</div>
......@@ -220,21 +216,108 @@ const CardSelect: React.FC = () => {
);
};
/** 正在组卡的zone,包括main/extra/side */
const DeckZone: React.FC<{
type: "main" | "extra" | "side";
}> = ({ type }) => {
const cards = useSnapshot(editDeckStore)[type];
const [_, dropRef] = useDrop({
accept: ["Card"], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type
// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: ({
value,
source,
}: {
value: CardMeta;
source: "main" | "extra" | "side" | "search";
}) => {
editDeckStore.add(type, value);
if (source !== "search") {
editDeckStore.remove(source, value);
}
},
});
return (
<div className={styles[type]} ref={dropRef}>
<div className={styles["card-continer"]}>
{cards.map((item, i) => (
<Card value={item} key={i} source={type} />
))}
</div>
</div>
);
};
/** 搜索区的搜索结果,使用memo避免重复渲染 */
const SearchResults: React.FC<{
results: CardMeta[];
}> = memo(({ results }) => (
<div className={styles["search-cards"]}>
}> = memo(({ results }) => {
const [_, dropRef] = useDrop({
accept: ["Card"], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type
// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: ({
value,
source,
}: {
value: CardMeta;
source: "main" | "extra" | "side" | "search";
}) => {
if (source !== "search") {
editDeckStore.remove(source, value);
}
},
});
return (
<div className={styles["search-cards"]} ref={dropRef}>
{results.map((item) => (
<Card value={item} key={v4uuid()} />
<Card value={item} key={v4uuid()} source="search" />
))}
</div>
));
);
});
/** 本组件内使用的单张卡片,增加了文字在图片下方 */
const Card: React.FC<{ value: CardMeta }> = memo(({ value }) => (
<div className={styles.card}>
const Card: React.FC<{
value: CardMeta;
source: "main" | "extra" | "side" | "search";
}> = memo(({ value, source }) => {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({
type: "Card",
item: { value, source },
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
});
drag(ref);
return (
<div className={styles.card} ref={ref}>
<div className={styles.cardname}>{value.text.name}</div>
<YgoCard className={styles.cardcover} code={value.id} />
</div>
));
);
});
const editDeckStore = proxy({
deckName: "",
main: [] as CardMeta[],
extra: [] as CardMeta[],
side: [] as CardMeta[],
add(type: "main" | "extra" | "side", card: CardMeta) {
editDeckStore[type].push(card);
},
remove(type: "main" | "extra" | "side", card: CardMeta) {
const index = editDeckStore[type].findIndex((item) => item.id === card.id);
if (index !== -1) {
editDeckStore[type].splice(index, 1);
}
},
set(deck: EditingDeck) {
editDeckStore.deckName = deck.deckName;
editDeckStore.main = deck.main;
editDeckStore.extra = deck.extra;
editDeckStore.side = deck.side;
},
}) satisfies EditingDeck;
import { Avatar } from "antd";
import classNames from "classnames";
import React, { useEffect } from "react";
import {
type LoaderFunction,
......@@ -19,7 +20,6 @@ import {
initSqlite,
initWASM,
} from "./utils";
import classNames from "classnames";
const NeosConfig = useConfig();
......
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