Commit 4cc684e9 authored by timel's avatar timel

feat: upload deck

parent ce4287dc
Pipeline #23068 passed with stages
in 13 minutes and 56 seconds
...@@ -25,8 +25,14 @@ ...@@ -25,8 +25,14 @@
.ant-modal-confirm-content { .ant-modal-confirm-content {
max-width: 100% !important; max-width: 100% !important;
width: 100%;
} }
.ant-select-dropdown { .ant-select-dropdown {
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.ant-modal-confirm .ant-modal-confirm-btns {
display: flex;
justify-content: end;
}
...@@ -2,18 +2,11 @@ import { ...@@ -2,18 +2,11 @@ import {
DeleteOutlined, DeleteOutlined,
DownloadOutlined, DownloadOutlined,
FileAddOutlined, FileAddOutlined,
InboxOutlined,
PlusOutlined, PlusOutlined,
UploadOutlined, CopyOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { import { App, Button, Dropdown, MenuProps, Upload, UploadProps } from "antd";
App,
Button,
Dropdown,
Input,
MenuProps,
Upload,
UploadProps,
} from "antd";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import YGOProDeck from "ygopro-deck-encode"; import YGOProDeck from "ygopro-deck-encode";
...@@ -24,75 +17,105 @@ import styles from "./DeckSelect.module.scss"; ...@@ -24,75 +17,105 @@ import styles from "./DeckSelect.module.scss";
export const DeckSelect: React.FC<{ export const DeckSelect: React.FC<{
decks: readonly { deckName: string }[]; decks: readonly { deckName: string }[];
selected: string; selected: string;
onSelect: (deckName: string) => void; onSelect: (deckName: string) => any;
onDelete: (deckName: string) => void; onDelete: (deckName: string) => Promise<any>;
onDownload: (deckName: string) => void; onDownload: (deckName: string) => any;
onAdd: () => void; }> = ({ decks, selected, onSelect, onDelete, onDownload }) => {
}> = ({ decks, selected, onSelect, onDelete, onDownload, onAdd }) => { const newDeck = useRef<IDeck[]>([]);
const newDeck = useRef<IDeck | null>(null); const { modal, message } = App.useApp();
const newDeckName = useRef<string | null>(null);
const { modal } = App.useApp(); /** 创建卡组,直接给一个命名,用户可以手动修改,无需modal打断流程 */
const modalProps = { width: 500, centered: true, icon: null }; const createNewDeck = async () => {
const showCreateModal = () => { const deckName = new Date().toLocaleString();
const { destroy } = modal.info({ await deckStore.add({
...modalProps, deckName,
title: "请输入新卡组名称", main: [],
content: ( extra: [],
<Input side: [],
onChange={(e) => {
newDeckName.current = e.target.value;
}}
/>
),
okText: "新建",
onCancel: () => destroy(),
onOk: async () => {
if (newDeckName.current && newDeckName.current !== "") {
await deckStore.add({
deckName: newDeckName.current,
main: [],
extra: [],
side: [],
});
}
},
}); });
onSelect(deckName);
}; };
const showUploadModal = () => {
const { destroy } = modal.info({ const showUploadModal = () =>
...modalProps, modal.info({
title: "请上传YDK文件", width: 600,
centered: true,
icon: null,
content: ( content: (
<DeckUploader <DeckUploader
onLoaded={(deck) => { onLoaded={(deck) => {
newDeck.current = deck; newDeck.current.push(deck);
}} }}
/> />
), ),
okText: "上传", okText: "上传",
onCancel: () => destroy(), maskClosable: true,
onOk: async () => { onOk: async () => {
if (newDeck.current) { const newDecks = await Promise.all(
await deckStore.add(newDeck.current); newDeck.current.map((deck) => deckStore.add(deck))
} );
newDecks.every(Boolean)
? message.success("上传成功")
: message.error("部分文件上传失败");
}, },
}); });
/** 从剪贴板导入。为什么错误处理这么丑陋... */
const importFromClipboard = () => {
// 检查浏览器是否支持 Clipboard API
if (navigator.clipboard) {
// 获取剪贴板内容
navigator.clipboard
.readText()
.then((text) => {
const deck = YGOProDeck.fromYdkString(text);
if (
!(deck.main.length + deck.extra.length + deck.side.length === 0)
) {
// YDK解析成功
const deckName = new Date().toLocaleString();
deckStore
.add({
deckName,
...deck,
})
.then((result) => {
if (result) {
message.success(`导入成功,卡组名为:${deckName}`);
onSelect(deckName);
} else {
message.error(`解析失败,请检查格式是否正确。`);
}
});
} else {
message.error(`解析失败,请检查格式是否正确。`);
}
})
.catch((err) => {
message.error("无法读取剪贴板内容:", err);
});
} else {
message.error("浏览器不支持 Clipboard API");
}
}; };
const items: MenuProps["items"] = [ const items: MenuProps["items"] = [
{ {
key: "1",
label: "新建卡组", label: "新建卡组",
icon: <PlusOutlined />, icon: <PlusOutlined />,
onClick: showCreateModal, onClick: createNewDeck,
}, },
{ {
key: "2", label: "从本地文件导入",
label: "导入卡组",
icon: <FileAddOutlined />, icon: <FileAddOutlined />,
onClick: showUploadModal, onClick: showUploadModal,
}, },
]; {
label: "从剪贴板导入",
icon: <CopyOutlined />,
onClick: importFromClipboard,
},
].map((_, key) => ({ ..._, key }));
return ( return (
<> <>
...@@ -111,7 +134,10 @@ export const DeckSelect: React.FC<{ ...@@ -111,7 +134,10 @@ export const DeckSelect: React.FC<{
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
type="text" type="text"
size="small" size="small"
onClick={cancelBubble(() => onDelete(deckName))} onClick={cancelBubble(async () => {
await onDelete(deckName);
onSelect(decks[0].deckName);
})}
/> />
<Button <Button
icon={<DownloadOutlined />} icon={<DownloadOutlined />}
...@@ -129,7 +155,6 @@ export const DeckSelect: React.FC<{ ...@@ -129,7 +155,6 @@ export const DeckSelect: React.FC<{
icon={<PlusOutlined />} icon={<PlusOutlined />}
shape="circle" shape="circle"
type="text" type="text"
onClick={onAdd}
size="large" size="large"
/> />
</Dropdown> </Dropdown>
...@@ -141,31 +166,29 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({ ...@@ -141,31 +166,29 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({
onLoaded, onLoaded,
}) => { }) => {
const [uploadState, setUploadState] = useState(""); const [uploadState, setUploadState] = useState("");
const { message } = App.useApp();
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
name: "file", name: "file",
multiple: true,
onChange(info) { onChange(info) {
if (uploadState != "ERROR") { if (uploadState != "ERROR") {
info.file.status = "done"; info.file.status = "done";
} }
}, },
accept: ".ydk",
beforeUpload(file, _) { beforeUpload(file, _) {
console.log({ file });
const reader = new FileReader(); const reader = new FileReader();
reader.readAsText(file); reader.readAsText(file);
reader.onload = (e) => { reader.onload = (e) => {
const ydk = e.target?.result as string; const ydk = e.target?.result as string;
const deck = YGOProDeck.fromYdkString(ydk); const deck = YGOProDeck.fromYdkString(ydk);
if ( if (!(deck.main.length + deck.extra.length + deck.side.length === 0)) {
!(
deck.main.length === 0 &&
deck.extra.length === 0 &&
deck.side.length === 0
)
) {
// YDK解析成功 // YDK解析成功
onLoaded({ deckName: file.name.replace(/\.ydk/g, ""), ...deck }); onLoaded({ deckName: file.name.replace(/\.ydk/g, ""), ...deck });
} else { } else {
alert(`${file.name}解析失败,请检查格式是否正确。`); message.error(`${file.name}解析失败,请检查格式是否正确。`);
setUploadState("ERROR"); setUploadState("ERROR");
} }
}; };
...@@ -173,9 +196,18 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({ ...@@ -173,9 +196,18 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({
}; };
return ( return (
<Upload {...uploadProps}> <div>
<Button icon={<UploadOutlined />}>点击上传</Button> <Upload.Dragger
</Upload> {...uploadProps}
style={{ width: "100%", margin: "20px 0 10px" }}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">单击或拖动文件到此区域进行上传</p>
<p className="ant-upload-hint">仅支持后缀名为ydk的卡组文件。</p>
</Upload.Dragger>
</div>
); );
}; };
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
padding: 8px;
} }
.btns { .btns {
...@@ -28,7 +29,7 @@ ...@@ -28,7 +29,7 @@
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
padding: 50px 0 10px; padding: 30px 0 10px;
& > button { & > button {
width: 220px; width: 220px;
border-radius: 3px; border-radius: 3px;
......
...@@ -104,11 +104,8 @@ export const Component: React.FC = () => { ...@@ -104,11 +104,8 @@ export const Component: React.FC = () => {
onDelete={async (name) => await deckStore.delete(name)} onDelete={async (name) => await deckStore.delete(name)}
onDownload={(name) => { onDownload={(name) => {
const deck = deckStore.get(name); const deck = deckStore.get(name);
if (deck) { if (deck) downloadDeckAsYDK(deck);
downloadDeckAsYDK(deck);
}
}} }}
onAdd={() => console.log("add")}
/> />
</ScrollableArea> </ScrollableArea>
<HigherCardDetail /> <HigherCardDetail />
......
...@@ -14,7 +14,7 @@ export const theme: ThemeConfig = { ...@@ -14,7 +14,7 @@ export const theme: ThemeConfig = {
Modal: { Modal: {
colorBgElevated: "#1f2531", colorBgElevated: "#1f2531",
paddingMD: 24, paddingMD: 24,
paddingContentHorizontalLG: 48, paddingContentHorizontalLG: 36,
}, },
Select: { Select: {
colorBgElevated: "hsla(0, 0%, 20%, 0.3)", colorBgElevated: "hsla(0, 0%, 20%, 0.3)",
......
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