Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
N
Neos
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
1
Issues
1
List
Boards
Labels
Service Desk
Milestones
Merge Requests
2
Merge Requests
2
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
MyCard
Neos
Commits
7f197346
Commit
7f197346
authored
Aug 31, 2023
by
Chunchi Che
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
代码结构优化:解耦DeckZone和DeckCard,复用在组卡页和Side页
parent
e23ceeb3
Changes
27
Show whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
621 additions
and
432 deletions
+621
-432
src/service/duel/announce.ts
src/service/duel/announce.ts
+17
-21
src/service/duel/rockPaperScissors.ts
src/service/duel/rockPaperScissors.ts
+9
-5
src/service/duel/selectOption.ts
src/service/duel/selectOption.ts
+3
-2
src/stores/matStore/methods/fetchHint.ts
src/stores/matStore/methods/fetchHint.ts
+8
-10
src/ui/BuildDeck/index.module.scss
src/ui/BuildDeck/index.module.scss
+1
-66
src/ui/BuildDeck/index.tsx
src/ui/BuildDeck/index.tsx
+31
-116
src/ui/BuildDeck/store.ts
src/ui/BuildDeck/store.ts
+2
-1
src/ui/BuildDeck/utils.ts
src/ui/BuildDeck/utils.ts
+0
-2
src/ui/Duel/Main.tsx
src/ui/Duel/Main.tsx
+11
-11
src/ui/Duel/Message/AnnounceModal.tsx
src/ui/Duel/Message/AnnounceModal.tsx
+0
-80
src/ui/Duel/Message/OptionModal.tsx
src/ui/Duel/Message/OptionModal.tsx
+84
-19
src/ui/Duel/Message/SelectCardsModal/index.tsx
src/ui/Duel/Message/SelectCardsModal/index.tsx
+29
-15
src/ui/Duel/Message/index.ts
src/ui/Duel/Message/index.ts
+0
-1
src/ui/Duel/PlayMat/Card/index.tsx
src/ui/Duel/PlayMat/Card/index.tsx
+2
-2
src/ui/Layout/index.tsx
src/ui/Layout/index.tsx
+1
-1
src/ui/Match/index.tsx
src/ui/Match/index.tsx
+8
-2
src/ui/NeosRouter.tsx
src/ui/NeosRouter.tsx
+4
-0
src/ui/Shared/DeckCard/index.module.scss
src/ui/Shared/DeckCard/index.module.scss
+38
-0
src/ui/Shared/DeckCard/index.tsx
src/ui/Shared/DeckCard/index.tsx
+56
-0
src/ui/Shared/DeckZone/index.module.scss
src/ui/Shared/DeckZone/index.module.scss
+39
-0
src/ui/Shared/DeckZone/index.tsx
src/ui/Shared/DeckZone/index.tsx
+89
-0
src/ui/Shared/index.ts
src/ui/Shared/index.ts
+2
-0
src/ui/Side/ChangeSideModal/index.tsx
src/ui/Side/ChangeSideModal/index.tsx
+0
-75
src/ui/Side/TpModal.module.scss
src/ui/Side/TpModal.module.scss
+0
-0
src/ui/Side/TpModal.tsx
src/ui/Side/TpModal.tsx
+1
-1
src/ui/Side/index.module.scss
src/ui/Side/index.module.scss
+55
-0
src/ui/Side/index.tsx
src/ui/Side/index.tsx
+131
-2
No files found.
src/service/duel/announce.ts
View file @
7f197346
import
{
fetchCard
,
fetchStrings
,
Region
,
ygopro
}
from
"
@/api
"
;
import
{
display
Announce
Modal
}
from
"
@/ui/Duel/Message
"
;
import
{
display
Option
Modal
}
from
"
@/ui/Duel/Message
"
;
import
MsgAnnounce
=
ygopro
.
StocGameMessage
.
MsgAnnounce
;
export
default
async
(
announce
:
MsgAnnounce
)
=>
{
...
...
@@ -14,26 +14,26 @@ export default async (announce: MsgAnnounce) => {
switch
(
type_
)
{
case
MsgAnnounce
.
AnnounceType
.
RACE
:
{
await
displayAnnounceModal
({
min
,
title
:
fetchStrings
(
Region
.
System
,
563
),
options
:
announce
.
options
.
map
((
option
)
=>
({
await
displayOptionModal
(
fetchStrings
(
Region
.
System
,
563
),
announce
.
options
.
map
((
option
)
=>
({
info
:
fetchStrings
(
Region
.
System
,
1200
+
option
.
code
),
response
:
option
.
response
,
})),
});
min
,
);
break
;
}
case
MsgAnnounce
.
AnnounceType
.
Attribute
:
{
await
displayAnnounceModal
({
min
,
title
:
fetchStrings
(
Region
.
System
,
562
),
options
:
announce
.
options
.
map
((
option
)
=>
({
await
displayOptionModal
(
fetchStrings
(
Region
.
System
,
562
),
announce
.
options
.
map
((
option
)
=>
({
info
:
fetchStrings
(
Region
.
System
,
1010
+
option
.
code
),
response
:
option
.
response
,
})),
});
min
,
);
break
;
}
...
...
@@ -48,23 +48,19 @@ export default async (announce: MsgAnnounce) => {
});
}
}
await
displayAnnounceModal
({
min
,
title
:
fetchStrings
(
Region
.
System
,
564
),
options
,
});
await
displayOptionModal
(
fetchStrings
(
Region
.
System
,
564
),
options
,
min
);
break
;
}
case
MsgAnnounce
.
AnnounceType
.
Number
:
{
await
displayAnnounceModal
({
min
,
title
:
fetchStrings
(
Region
.
System
,
565
),
options
:
announce
.
options
.
map
((
option
)
=>
({
await
displayOptionModal
(
fetchStrings
(
Region
.
System
,
565
),
announce
.
options
.
map
((
option
)
=>
({
info
:
option
.
code
.
toString
(),
response
:
option
.
response
,
})),
});
min
,
);
break
;
}
...
...
src/service/duel/rockPaperScissors.ts
View file @
7f197346
...
...
@@ -5,9 +5,13 @@ export default async (mora: ygopro.StocGameMessage.MsgRockPaperScissors) => {
const
_player
=
mora
.
player
;
// TODO: I18n
await
displayOptionModal
(
"
请选择猜拳
"
,
[
{
msg
:
"
剪刀
"
,
response
:
1
},
{
msg
:
"
石头
"
,
response
:
2
},
{
msg
:
"
布
"
,
response
:
3
},
]);
await
displayOptionModal
(
"
请选择猜拳
"
,
[
{
info
:
"
剪刀
"
,
response
:
1
},
{
info
:
"
石头
"
,
response
:
2
},
{
info
:
"
布
"
,
response
:
3
},
],
1
,
);
};
src/service/duel/selectOption.ts
View file @
7f197346
...
...
@@ -14,9 +14,10 @@ export default async (selectOption: ygopro.StocGameMessage.MsgSelectOption) => {
await
Promise
.
all
(
options
.
map
(
async
({
code
,
response
})
=>
{
const
meta
=
fetchCard
(
code
>>
4
);
const
msg
=
getCardStr
(
meta
,
code
&
0xf
)
||
"
[?]
"
;
return
{
msg
,
response
};
const
info
=
getCardStr
(
meta
,
code
&
0xf
)
??
"
[?]
"
;
return
{
info
,
response
};
}),
),
1
,
);
};
src/stores/matStore/methods/fetchHint.ts
View file @
7f197346
...
...
@@ -5,11 +5,9 @@ import { cardStore } from "@/stores/cardStore";
import
{
matStore
}
from
"
../store
"
;
const
{
hint
}
=
matStore
;
export
const
fetchCommonHintMeta
=
(
code
:
number
)
=>
{
hint
.
code
=
code
;
hint
.
msg
=
fetchStrings
(
Region
.
System
,
code
);
matStore
.
hint
.
code
=
code
;
matStore
.
hint
.
msg
=
fetchStrings
(
Region
.
System
,
code
);
};
export
const
fetchSelectHintMeta
=
async
({
...
...
@@ -31,13 +29,13 @@ export const fetchSelectHintMeta = async ({
selectHintMeta
=
await
getStrings
(
selectHintData
);
}
hint
.
code
=
selectHintData
;
if
(
hint
.
code
>
DESCRIPTION_LIMIT
)
{
matStore
.
hint
.
code
=
selectHintData
;
if
(
matStore
.
hint
.
code
>
DESCRIPTION_LIMIT
)
{
// 针对`MSG_SELECT_PLACE`的特化逻辑
hint
.
msg
=
selectHintMeta
;
matStore
.
hint
.
msg
=
selectHintMeta
;
}
else
{
hint
.
esSelectHint
=
selectHintMeta
;
hint
.
esHint
=
esHint
;
matStore
.
hint
.
esSelectHint
=
selectHintMeta
;
matStore
.
hint
.
esHint
=
esHint
;
}
};
...
...
@@ -74,5 +72,5 @@ export const fetchEsHintMeta = async ({
}
}
hint
.
esHint
=
esHint
;
matStore
.
hint
.
esHint
=
esHint
;
};
src/ui/BuildDeck/index.module.scss
View file @
7f197346
...
...
@@ -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
{
...
...
src/ui/BuildDeck/index.tsx
View file @
7f197346
...
...
@@ -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
,
useDr
ag
,
useDr
op
}
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
)
{
...
...
@@ -207,7 +204,23 @@ export const DeckEditor: React.FC<{
</
Space
>
<
ScrollableArea
className=
{
styles
[
"
deck-zone
"
]
}
>
{
([
"
main
"
,
"
extra
"
,
"
side
"
]
as
const
).
map
((
type
)
=>
(
<
DeckZone
key=
{
type
}
type=
{
type
}
/>
<
DeckZone
key=
{
type
}
type=
{
type
}
cards=
{
[...
snapEditDeck
[
type
]]
}
canAdd=
{
editDeckStore
.
canAdd
}
onChange=
{
(
card
,
source
,
destination
)
=>
{
editDeckStore
.
add
(
destination
,
card
);
if
(
source
!==
"
search
"
)
{
editDeckStore
.
remove
(
source
,
card
);
}
}
}
onElementClick=
{
(
card
)
=>
{
selectedCard
.
id
=
card
.
id
;
selectedCard
.
open
=
true
;
}
}
onElementRightClick=
{
(
card
)
=>
editDeckStore
.
remove
(
type
,
card
)
}
/>
))
}
</
ScrollableArea
>
</
div
>
...
...
@@ -394,65 +407,6 @@ const Search: React.FC = () => {
);
};
/** 正在组卡的zone,包括main/extra/side */
const
DeckZone
:
React
.
FC
<
{
type
:
Type
;
}
>
=
({
type
})
=>
{
const
{
message
}
=
App
.
useApp
();
const
cards
=
useSnapshot
(
editDeckStore
)[
type
];
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
}
=
editDeckStore
.
canAdd
(
value
,
type
,
source
);
if
(
result
)
{
editDeckStore
.
add
(
type
,
value
);
if
(
source
!==
"
search
"
)
{
editDeckStore
.
remove
(
source
,
value
);
}
}
else
{
message
.
error
(
reason
);
}
},
hover
:
({
value
,
source
})
=>
{
setAllowToDrop
(
type
!==
source
?
editDeckStore
.
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
)
=>
(
<
Card
value=
{
card
}
key=
{
card
.
id
+
i
+
type
}
source=
{
type
}
onRightClick=
{
()
=>
editDeckStore
.
remove
(
type
,
card
)
}
/>
))
}
<
div
className=
{
styles
[
"
editing-zone-name
"
]
}
>
{
`${type.toUpperCase()}: ${cards.length}`
}
</
div
>
</
div
>
</
div
>
);
};
/** 搜索区的搜索结果,使用memo避免重复渲染 */
const
SearchResults
:
React
.
FC
<
{
results
:
CardMeta
[];
...
...
@@ -473,7 +427,15 @@ const SearchResults: React.FC<{
<>
<
div
className=
{
styles
[
"
search-cards
"
]
}
>
{
currentData
.
map
((
card
)
=>
(
<
Card
value=
{
card
}
key=
{
card
.
id
}
source=
"search"
/>
<
DeckCard
value=
{
card
}
key=
{
card
.
id
}
source=
"search"
onClick=
{
()
=>
{
selectedCard
.
id
=
card
.
id
;
selectedCard
.
open
=
true
;
}
}
/>
))
}
</
div
>
{
results
.
length
>
itemsPerPage
&&
(
...
...
@@ -496,53 +458,6 @@ const SearchResults: React.FC<{
);
});
/** 本组件内使用的单张卡片,增加了文字在图片下方 */
const
Card
:
React
.
FC
<
{
value
:
CardMeta
;
source
:
Type
|
"
search
"
;
onRightClick
?:
()
=>
void
;
}
>
=
memo
(({
value
,
source
,
onRightClick
})
=>
{
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=
{
()
=>
{
selectedCard
.
id
=
value
.
id
;
selectedCard
.
open
=
true
;
}
}
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
(
...
...
src/ui/BuildDeck/store.ts
View file @
7f197346
...
...
@@ -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
:
""
,
...
...
src/ui/BuildDeck/utils.ts
View file @
7f197346
...
...
@@ -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
;
...
...
src/ui/Duel/Main.tsx
View file @
7f197346
import
React
,
{
useEffect
}
from
"
react
"
;
import
{
useNavigate
}
from
"
react-router-dom
"
;
import
{
useSnapshot
}
from
"
valtio
"
;
import
{
resetUnivers
e
}
from
"
@/stores
"
;
import
{
SideStage
,
sideStor
e
}
from
"
@/stores
"
;
import
{
ChangeSideModal
,
TpModal
}
from
"
../Side
"
;
import
{
Alert
,
AnnounceModal
,
CardListModal
,
CardModal
,
CheckCounterModal
,
...
...
@@ -21,12 +21,15 @@ import {
import
{
LifeBar
,
Mat
,
Menu
,
Underlying
}
from
"
./PlayMat
"
;
export
const
Component
:
React
.
FC
=
()
=>
{
const
{
stage
}
=
useSnapshot
(
sideStore
);
const
navigate
=
useNavigate
();
useEffect
(()
=>
{
return
()
=>
{
//
Duel组件卸载的时候初始化一些stor
e
resetUniverse
(
);
}
;
},
[]);
if
(
stage
===
SideStage
.
SIDE_CHANGING
)
{
//
跳转更换Sid
e
navigate
(
"
/side
"
);
}
},
[
stage
]);
return
(
<>
...
...
@@ -44,11 +47,8 @@ export const Component: React.FC = () => {
<
OptionModal
/>
<
CheckCounterModal
/>
<
SortCardModal
/>
<
AnnounceModal
/>
<
SimpleSelectCardsModal
/>
<
EndModal
/>
<
ChangeSideModal
/>
<
TpModal
/>
</>
);
};
...
...
src/ui/Duel/Message/AnnounceModal.tsx
deleted
100644 → 0
View file @
e23ceeb3
import
{
CheckCard
}
from
"
@ant-design/pro-components
"
;
import
{
Button
}
from
"
antd
"
;
import
React
,
{
useState
}
from
"
react
"
;
import
{
proxy
,
useSnapshot
}
from
"
valtio
"
;
import
{
sendSelectOptionResponse
}
from
"
@/api
"
;
import
{
NeosModal
}
from
"
./NeosModal
"
;
interface
AnnounceModalProps
{
isOpen
:
boolean
;
title
?:
string
;
min
:
number
;
options
:
{
info
:
string
;
response
:
number
;
}[];
}
const
defaultProps
=
{
isOpen
:
false
,
min
:
1
,
options
:
[],
};
const
localStore
=
proxy
<
AnnounceModalProps
>
(
defaultProps
);
export
const
AnnounceModal
=
()
=>
{
const
{
isOpen
,
title
,
min
,
options
}
=
useSnapshot
(
localStore
);
const
[
selected
,
setSelected
]
=
useState
<
number
[]
>
([]);
return
(
<
NeosModal
title=
{
title
}
open=
{
isOpen
}
footer=
{
<
Button
disabled=
{
selected
.
length
!==
min
}
onClick=
{
()
=>
{
let
response
=
selected
.
reduce
((
res
,
current
)
=>
res
|
current
,
0
);
// 多个选择求或
sendSelectOptionResponse
(
response
);
rs
();
}
}
>
submit
</
Button
>
}
>
<
CheckCard
.
Group
bordered
multiple
size=
"small"
onChange=
{
(
value
:
any
)
=>
{
setSelected
(
value
);
}
}
>
{
options
.
map
((
option
,
idx
)
=>
(
<
CheckCard
key=
{
idx
}
title=
{
option
.
info
}
value=
{
option
.
response
}
/>
))
}
</
CheckCard
.
Group
>
</
NeosModal
>
);
};
let
rs
:
(
arg
?:
any
)
=>
void
=
()
=>
{};
export
const
displayAnnounceModal
=
async
(
args
:
Omit
<
AnnounceModalProps
,
"
isOpen
"
>
,
)
=>
{
Object
.
entries
(
args
).
forEach
(([
key
,
value
])
=>
{
// @ts-ignore
localStore
[
key
]
=
value
;
});
localStore
.
isOpen
=
true
;
await
new
Promise
<
void
>
((
resolve
)
=>
(
rs
=
resolve
));
// 等待在组件内resolve
localStore
.
isOpen
=
false
;
localStore
.
min
=
1
;
localStore
.
options
=
[];
localStore
.
title
=
undefined
;
};
src/ui/Duel/Message/OptionModal.tsx
View file @
7f197346
import
{
CheckCard
}
from
"
@ant-design/pro-components
"
;
import
{
Button
}
from
"
antd
"
;
import
React
,
{
useState
}
from
"
react
"
;
import
{
Button
,
Segmented
}
from
"
antd
"
;
import
{
chunk
}
from
"
lodash-es
"
;
import
React
,
{
useEffect
,
useState
}
from
"
react
"
;
import
{
proxy
,
useSnapshot
}
from
"
valtio
"
;
import
{
...
...
@@ -14,52 +15,116 @@ import {
import
{
NeosModal
}
from
"
./NeosModal
"
;
type
Options
=
{
msg
:
string
;
response
:
number
}[];
type
Options
=
{
info
:
string
;
response
:
number
}[];
const
defaultStore
=
{
title
:
""
,
isOpen
:
false
,
min
:
1
,
options
:
[]
satisfies
Options
as
Options
,
};
const
store
=
proxy
(
defaultStore
);
// 一页最多4个选项
const
MAX_NUM_PER_PAGE
=
4
;
export
const
OptionModal
=
()
=>
{
const
snap
=
useSnapshot
(
store
);
const
{
title
,
isOpen
,
min
,
options
}
=
snap
;
// options可能太多,因此分页展示
const
[
page
,
setPage
]
=
useState
(
0
);
const
maxPage
=
Math
.
ceil
(
options
.
length
/
MAX_NUM_PER_PAGE
);
const
[
selecteds
,
setSelecteds
]
=
useState
<
number
[][]
>
([]);
const
grouped
=
chunk
(
options
,
MAX_NUM_PER_PAGE
);
const
{
title
,
isOpen
,
options
}
=
snap
;
const
[
selected
,
setSelected
]
=
useState
<
number
|
undefined
>
(
undefined
);
const
onClick
=
()
=>
{
if
(
selected
!==
undefined
)
{
sendSelectOptionResponse
(
selected
);
const
onSummit
=
()
=>
{
const
responses
=
selecteds
.
flat
();
if
(
responses
.
length
>
0
)
{
const
response
=
responses
.
reduce
((
res
,
current
)
=>
res
|
current
,
0
);
// 多个选择求或
sendSelectOptionResponse
(
response
);
rs
();
}
};
useEffect
(()
=>
{
setSelecteds
(
Array
.
from
({
length
:
maxPage
}).
map
((
_
)
=>
[]));
},
[
options
]);
return
(
<
NeosModal
title=
{
title
}
open=
{
isOpen
}
footer=
{
<
Button
disabled=
{
selected
===
undefined
}
onClick=
{
onClick
}
>
<
Button
disabled=
{
selected
s
.
flat
().
length
!==
min
}
onClick=
{
onSummit
}
>
确定
</
Button
>
}
>
<
CheckCard
.
Group
bordered
size=
"small"
onChange=
{
setSelected
as
any
}
>
<
Selector
page=
{
page
}
maxPage=
{
maxPage
}
onChange=
{
setPage
as
any
}
/>
{
grouped
.
map
(
(
options
,
i
)
=>
i
===
page
&&
(
<
CheckCard
.
Group
key=
{
i
}
bordered
multiple
value=
{
selecteds
[
i
]
}
style=
{
{
display
:
"
grid
"
,
gridTemplateColumns
:
"
repeat(2, 1fr)
"
,
gap
:
"
10px
"
,
}
}
onChange=
{
(
values
:
any
)
=>
{
const
v
=
selecteds
.
map
((
x
,
i
)
=>
(
i
===
page
?
values
:
x
));
setSelecteds
(
v
);
}
}
>
{
options
.
map
((
option
,
idx
)
=>
(
<
CheckCard
key=
{
idx
}
title=
{
option
.
msg
}
value=
{
option
.
response
}
/>
<
CheckCard
key=
{
idx
}
style=
{
{
width
:
"
200px
"
,
marginInlineEnd
:
0
,
marginBlockEnd
:
0
,
}
}
title=
{
option
.
info
}
value=
{
option
.
response
}
/>
))
}
</
CheckCard
.
Group
>
),
)
}
</
NeosModal
>
);
};
/* 选择区域 */
const
Selector
:
React
.
FC
<
{
page
:
number
;
maxPage
:
number
;
onChange
:
(
value
:
number
)
=>
void
;
}
>
=
({
page
,
maxPage
,
onChange
})
=>
maxPage
>
1
?
(
<
Segmented
block
options=
{
Array
.
from
({
length
:
maxPage
}).
map
((
_
,
idx
)
=>
idx
)
}
style=
{
{
margin
:
"
10px 0
"
}
}
value=
{
page
}
onChange=
{
onChange
as
any
}
></
Segmented
>
)
:
(
<></>
);
let
rs
:
(
v
?:
any
)
=>
void
=
()
=>
{};
export
const
displayOptionModal
=
async
(
title
:
string
,
options
:
Options
)
=>
{
export
const
displayOptionModal
=
async
(
title
:
string
,
options
:
Options
,
min
:
number
,
)
=>
{
store
.
title
=
title
;
store
.
options
=
options
;
store
.
min
=
min
;
store
.
isOpen
=
true
;
await
new
Promise
((
resolve
)
=>
(
rs
=
resolve
));
store
.
isOpen
=
false
;
...
...
@@ -87,10 +152,10 @@ export const handleEffectActivation = async (
?
getCardStr
(
meta
,
effect
.
effectCode
&
0xf
)
??
"
[:?]
"
:
"
[:?]
"
;
return
{
msg
:
effectMsg
,
info
:
effectMsg
,
response
:
effect
.
response
,
};
});
await
displayOptionModal
(
fetchStrings
(
Region
.
System
,
556
),
options
);
// 主动发动效果,所以不需要await,但是以后可能要留心
await
displayOptionModal
(
fetchStrings
(
Region
.
System
,
556
),
options
,
1
);
// 主动发动效果,所以不需要await,但是以后可能要留心
}
};
src/ui/Duel/Message/SelectCardsModal/index.tsx
View file @
7f197346
...
...
@@ -46,8 +46,8 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
onCancel
,
onFinish
,
})
=>
{
// FIXME: handle `selecteds`
const
[
result
,
setResult
]
=
useState
<
Option
[]
>
([]);
const
grouped
=
groupBy
(
selectables
,
(
option
)
=>
option
.
location
?.
zone
!
);
const
[
result
,
setResult
]
=
useState
<
[
ygopro
.
CardZone
,
Option
[]]
[]
>
([]);
const
[
submitable
,
setSubmitable
]
=
useState
(
false
);
const
hint
=
useSnapshot
(
matStore
.
hint
);
...
...
@@ -56,14 +56,21 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
const
minMaxText
=
min
===
max
?
min
:
`
${
min
}
-
${
max
}
`
;
// const isMultiple = !single && max > 1;
// FIXME: 如果想上面这样鞋会panic,还不是很清楚原因,先放着后面再优化
const
isMultiple
=
true
;
useEffect
(()
=>
{
const
initial
:
[
ygopro
.
CardZone
,
Option
[]][]
=
grouped
.
map
(([
zone
,
_
])
=>
[
zone
,
[]
as
Option
[],
]);
if
(
initial
.
length
>
0
)
{
setResult
(
initial
);
}
},
[
selectables
]);
// 判断是否可以提交
useEffect
(()
=>
{
const
flatResult
=
result
.
map
(([
_
,
v
])
=>
v
).
flat
();
const
[
sumLevel1
,
sumLevel2
]
=
([
"
level1
"
,
"
level2
"
]
as
const
).
map
((
key
)
=>
[...
mustSelects
,
...
r
esult
]
[...
mustSelects
,
...
flatR
esult
]
.
map
((
option
)
=>
option
[
key
]
||
0
)
.
reduce
((
sum
,
current
)
=>
sum
+
current
,
0
),
);
...
...
@@ -72,12 +79,10 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
:
sumLevel1
===
totalLevels
||
sumLevel2
===
totalLevels
;
setSubmitable
(
single
?
r
esult
.
length
===
1
:
result
.
length
>=
min
&&
r
esult
.
length
<=
max
&&
levelMatched
,
?
flatR
esult
.
length
===
1
:
flatResult
.
length
>=
min
&&
flatR
esult
.
length
<=
max
&&
levelMatched
,
);
},
[
result
.
length
]);
const
grouped
=
groupBy
(
selectables
,
(
option
)
=>
option
.
location
?.
zone
!
);
},
[
result
]);
const
zoneOptions
=
grouped
.
map
((
x
)
=>
({
value
:
x
[
0
],
...
...
@@ -126,7 +131,9 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
<
Button
type=
"primary"
disabled=
{
!
submitable
}
onClick=
{
()
=>
onSubmit
([...
mustSelects
,
...
result
])
}
onClick=
{
()
=>
onSubmit
([...
mustSelects
,
...
result
.
map
(([
_
,
v
])
=>
v
).
flat
()])
}
>
{
submitText
}
</
Button
>
...
...
@@ -144,11 +151,18 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
options
[
0
]
===
selectedZone
&&
(
<
div
className=
{
styles
[
"
container
"
]
}
key=
{
i
}
>
<
CheckCard
.
Group
onChange=
{
(
res
)
=>
{
setResult
((
isMultiple
?
res
:
[
res
])
as
any
);
onChange=
{
(
res
:
any
)
=>
{
const
newRes
:
[
ygopro
.
CardZone
,
Option
[]][]
=
result
.
map
(
([
k
,
v
])
=>
[
k
,
k
===
selectedZone
?
res
:
v
],
);
setResult
(
newRes
);
}
}
value=
{
result
.
find
(([
k
,
_
])
=>
k
===
selectedZone
)?.[
1
]
??
([]
as
any
)
}
// TODO 考虑如何设置默认值,比如只有一个的,就直接选中
multiple
=
{
isMultiple
}
multiple
className=
{
styles
[
"
check-group
"
]
}
>
{
options
[
1
].
map
((
card
,
j
)
=>
(
...
...
src/ui/Duel/Message/index.ts
View file @
7f197346
export
*
from
"
./Alert
"
;
export
*
from
"
./AnnounceModal
"
;
export
*
from
"
./CardListModal
"
;
export
*
from
"
./CardModal
"
;
export
*
from
"
./CheckCounterModal
"
;
...
...
src/ui/Duel/PlayMat/Card/index.tsx
View file @
7f197346
...
...
@@ -351,11 +351,11 @@ const handleEffectActivation = (
?
getCardStr
(
meta
,
effect
.
effectCode
&
0xf
)
??
"
[:?]
"
:
"
[:?]
"
;
return
{
msg
:
effectMsg
,
info
:
effectMsg
,
response
:
effect
.
response
,
};
});
displayOptionModal
(
fetchStrings
(
Region
.
System
,
556
),
options
);
// 主动发动效果,所以不需要await,但是以后可能要留心
displayOptionModal
(
fetchStrings
(
Region
.
System
,
556
),
options
,
1
);
// 主动发动效果,所以不需要await,但是以后可能要留心
}
}
;
...
...
src/ui/Layout/index.tsx
View file @
7f197346
...
...
@@ -64,7 +64,7 @@ export const Component = () => {
const
logined
=
Boolean
(
useSnapshot
(
accountStore
).
user
);
const
{
pathname
}
=
routerLocation
;
const
pathnamesHideHeader
=
[
"
/waitroom
"
,
"
/duel
"
];
const
pathnamesHideHeader
=
[
"
/waitroom
"
,
"
/duel
"
,
"
/side
"
];
const
callbackUrl
=
`
${
location
.
origin
}
/match/`
;
const
onLogin
=
()
=>
location
.
replace
(
getSSOSignInUrl
(
callbackUrl
));
...
...
src/ui/Match/index.tsx
View file @
7f197346
...
...
@@ -6,12 +6,12 @@ import {
}
from
"
@ant-design/icons
"
;
import
{
App
,
Button
,
Space
}
from
"
antd
"
;
import
{
useEffect
,
useState
}
from
"
react
"
;
import
{
useNavigate
}
from
"
react-router-dom
"
;
import
{
LoaderFunction
,
useNavigate
}
from
"
react-router-dom
"
;
import
{
useSnapshot
}
from
"
valtio
"
;
import
{
match
}
from
"
@/api
"
;
import
{
useConfig
}
from
"
@/config
"
;
import
{
accountStore
,
deckStore
,
roomStore
}
from
"
@/stores
"
;
import
{
accountStore
,
deckStore
,
r
esetUniverse
,
r
oomStore
}
from
"
@/stores
"
;
import
{
Background
,
IconFont
,
Select
}
from
"
@/ui/Shared
"
;
import
styles
from
"
./index.module.scss
"
;
...
...
@@ -21,6 +21,12 @@ import { connectSrvpro } from "./util";
const
NeosConfig
=
useConfig
();
export
const
loader
:
LoaderFunction
=
()
=>
{
// 在加载这个页面之前先重置一些store,清掉上局游戏遗留的数据
resetUniverse
();
return
null
;
};
export
const
Component
:
React
.
FC
=
()
=>
{
const
{
message
}
=
App
.
useApp
();
const
serverList
=
NeosConfig
.
servers
;
...
...
src/ui/NeosRouter.tsx
View file @
7f197346
...
...
@@ -28,6 +28,10 @@ const router = createBrowserRouter([
path
:
"
/duel
"
,
lazy
:
()
=>
import
(
"
./Duel/Main
"
),
},
{
path
:
"
/side
"
,
lazy
:
()
=>
import
(
"
./Side
"
),
},
],
},
]);
...
...
src/ui/Shared/DeckCard/index.module.scss
0 → 100644
View file @
7f197346
.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
;
}
}
src/ui/Shared/DeckCard/index.tsx
0 → 100644
View file @
7f197346
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
>
);
});
src/ui/Shared/DeckZone/index.module.scss
0 → 100644
View file @
7f197346
.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
;
}
src/ui/Shared/DeckZone/index.tsx
0 → 100644
View file @
7f197346
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
>
);
};
src/ui/Shared/index.ts
View file @
7f197346
export
*
from
"
./Background
"
;
export
*
from
"
./CardEffectText
"
;
export
*
from
"
./css
"
;
export
*
from
"
./DeckCard
"
;
export
*
from
"
./DeckZone
"
;
export
*
from
"
./IconFont
"
;
export
*
from
"
./Loading
"
;
export
*
from
"
./Scrollbar
"
;
...
...
src/ui/Side/ChangeSideModal/index.tsx
deleted
100644 → 0
View file @
e23ceeb3
import
{
App
,
Button
,
Modal
}
from
"
antd
"
;
import
React
,
{
useEffect
}
from
"
react
"
;
import
{
DndProvider
}
from
"
react-dnd
"
;
import
{
HTML5Backend
}
from
"
react-dnd-html5-backend
"
;
import
{
useSnapshot
}
from
"
valtio
"
;
import
{
CardMeta
,
sendUpdateDeck
}
from
"
@/api
"
;
import
{
roomStore
,
SideStage
,
sideStore
}
from
"
@/stores
"
;
import
{
DeckEditor
}
from
"
../../BuildDeck
"
;
import
{
editDeckStore
}
from
"
../../BuildDeck/store
"
;
import
{
iDeckToEditingDeck
}
from
"
../../BuildDeck/utils
"
;
import
{
Background
}
from
"
../../Shared
"
;
export
const
ChangeSideModal
:
React
.
FC
=
()
=>
{
const
{
message
}
=
App
.
useApp
();
const
{
deckName
,
main
,
extra
,
side
}
=
useSnapshot
(
editDeckStore
);
const
{
stage
}
=
useSnapshot
(
sideStore
);
const
{
errorMsg
}
=
useSnapshot
(
roomStore
);
const
cardMeta2Id
=
(
meta
:
CardMeta
)
=>
meta
.
id
;
const
handleSummit
=
()
=>
{
const
newDeck
=
{
deckName
:
deckName
,
main
:
main
.
map
(
cardMeta2Id
),
extra
:
extra
.
map
(
cardMeta2Id
),
side
:
side
.
map
(
cardMeta2Id
),
};
sendUpdateDeck
(
newDeck
);
editDeckStore
.
edited
=
false
;
};
useEffect
(()
=>
{
if
(
stage
===
SideStage
.
SIDE_CHANGED
)
{
message
.
info
(
"
副卡组更换成功,请耐心等待其他玩家更换卡组
"
);
}
},
[
stage
]);
useEffect
(()
=>
{
if
(
errorMsg
!==
undefined
&&
errorMsg
!==
""
)
{
message
.
error
(
errorMsg
);
roomStore
.
errorMsg
=
undefined
;
}
},
[
errorMsg
]);
return
(
<
Modal
title=
"请选择更换副卡组"
open=
{
stage
===
SideStage
.
SIDE_CHANGING
||
stage
===
SideStage
.
SIDE_CHANGED
}
width=
{
700
}
closable=
{
false
}
footer=
{
<
Button
disabled=
{
stage
>
SideStage
.
SIDE_CHANGING
}
onClick=
{
handleSummit
}
>
副卡组更换完毕
</
Button
>
}
>
<
DndProvider
backend=
{
HTML5Backend
}
>
<
Background
/>
<
DeckEditor
deck=
{
sideStore
.
deck
}
onClear=
{
()
=>
message
.
error
(
"
对局中清空卡组不怕找不回来吗?!
"
)
}
onSave=
{
()
=>
message
.
error
(
"
点击右下角按钮确认副卡组更换完毕
"
)
}
onReset=
{
async
()
=>
{
editDeckStore
.
set
(
await
iDeckToEditingDeck
(
sideStore
.
deck
));
}
}
/>
</
DndProvider
>
</
Modal
>
);
};
src/ui/Side/TpModal
/index
.module.scss
→
src/ui/Side/TpModal.module.scss
View file @
7f197346
File moved
src/ui/Side/TpModal
/index
.tsx
→
src/ui/Side/TpModal.tsx
View file @
7f197346
...
...
@@ -5,7 +5,7 @@ import { useSnapshot } from "valtio";
import
{
sendTpResult
}
from
"
@/api
"
;
import
{
SideStage
,
sideStore
}
from
"
@/stores
"
;
import
styles
from
"
./
index
.module.scss
"
;
import
styles
from
"
./
TpModal
.module.scss
"
;
export
const
TpModal
:
React
.
FC
=
()
=>
{
const
{
stage
}
=
useSnapshot
(
sideStore
);
...
...
src/ui/Side/index.module.scss
0 → 100644
View file @
7f197346
.container
{
display
:
flex
;
height
:
100%
;
}
.sider
{
width
:
var
(
--
sider-width
);
flex
:
0
0
var
(
--
sider-width
);
background-color
:
hsla
(
0
,
0%
,
100%
,
0
.05
);
position
:
relative
;
height
:
100%
;
max-height
:
100%
;
min-height
:
100%
;
}
.content
{
flex
:
1
;
padding-bottom
:
0
;
padding
:
1rem
;
width
:
660px
;
}
.deck-container
{
width
:
-
webkit-fill-available
;
height
:
calc
(
100%
-
20px
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0
.05
);
border-radius
:
10px
;
display
:
flex
;
flex-direction
:
column
;
&
>
*
:not
(
:last-of-type
)
{
border-bottom
:
1px
solid
rgba
(
255
,
255
,
255
,
0
.1
);
}
.title
{
height
:
44px
;
flex
:
0
0
44px
;
justify-content
:
space-between
;
padding
:
1em
2em
;
font-size
:
16px
;
}
.deck-zone
{
display
:
flex
;
flex-direction
:
column
;
height
:
100%
;
--card-grid
:
15
;
}
background-color
:
hsla
(
0
,
0%
,
100%
,
0
.05
);
backdrop-filter
:
blur
(
5px
);
}
.detail-container
{
--detail-width
:
300px
;
width
:
var
(
--
detail-width
);
flex
:
0
0
var
(
--
detail-width
);
position
:
relative
;
}
src/ui/Side/index.tsx
View file @
7f197346
export
*
from
"
./ChangeSideModal
"
;
export
*
from
"
./TpModal
"
;
import
{
CheckOutlined
,
UndoOutlined
}
from
"
@ant-design/icons
"
;
import
{
App
,
Button
,
Space
}
from
"
antd
"
;
import
React
,
{
useEffect
,
useState
}
from
"
react
"
;
import
{
DndProvider
}
from
"
react-dnd
"
;
import
{
HTML5Backend
}
from
"
react-dnd-html5-backend
"
;
import
{
useNavigate
}
from
"
react-router-dom
"
;
import
{
useSnapshot
}
from
"
valtio
"
;
import
{
CardMeta
,
fetchCard
,
sendUpdateDeck
}
from
"
@/api
"
;
import
{
isExtraDeckCard
}
from
"
@/common
"
;
import
{
IDeck
,
roomStore
,
SideStage
,
sideStore
}
from
"
@/stores
"
;
import
{
CardDetail
}
from
"
../BuildDeck/CardDetail
"
;
import
{
Background
,
DeckZone
,
ScrollableArea
,
Type
}
from
"
../Shared
"
;
import
{
Chat
}
from
"
../WaitRoom/Chat
"
;
import
styles
from
"
./index.module.scss
"
;
import
{
TpModal
}
from
"
./TpModal
"
;
export
const
Component
:
React
.
FC
=
()
=>
{
const
{
message
}
=
App
.
useApp
();
const
{
deck
:
sideDeck
}
=
sideStore
;
const
{
stage
}
=
useSnapshot
(
sideStore
);
const
{
errorMsg
}
=
useSnapshot
(
roomStore
);
const
initialDeck
=
JSON
.
parse
(
JSON
.
stringify
(
sideDeck
));
const
[
deck
,
setDeck
]
=
useState
<
IDeck
>
(
initialDeck
);
const
[
selectedCard
,
setSelectedCard
]
=
useState
(
0
);
const
navigate
=
useNavigate
();
const
canAdd
=
(
card
:
CardMeta
,
type
:
Type
,
_source
:
Type
|
"
search
"
)
=>
{
const
cardType
=
card
.
data
.
type
??
0
;
if
(
(
type
===
"
extra
"
&&
!
isExtraDeckCard
(
cardType
))
||
(
type
===
"
main
"
&&
isExtraDeckCard
(
cardType
))
)
{
return
{
result
:
false
,
reason
:
"
卡片种类不符合
"
};
}
else
{
return
{
result
:
true
,
reason
:
""
};
}
};
const
onChange
=
(
card
:
CardMeta
,
source
:
Type
|
"
search
"
,
destination
:
Type
,
)
=>
{
setDeck
((
prev
)
=>
{
const
deck
=
{
...
prev
};
if
(
source
!==
"
search
"
)
{
const
removeIndex
=
deck
[
source
].
findIndex
((
id
)
=>
id
===
card
.
id
);
if
(
removeIndex
!==
-
1
)
{
deck
[
source
].
splice
(
removeIndex
,
1
);
}
}
deck
[
destination
].
push
(
card
.
id
);
return
deck
;
});
};
const
onReset
=
()
=>
{
setDeck
(
JSON
.
parse
(
JSON
.
stringify
(
sideDeck
)));
message
.
info
(
"
重置成功
"
);
};
const
onSummit
=
()
=>
sendUpdateDeck
(
deck
);
useEffect
(()
=>
{
if
(
stage
===
SideStage
.
SIDE_CHANGED
)
{
message
.
info
(
"
副卡组更换成功,请耐心等待其他玩家更换卡组
"
);
}
if
(
stage
===
SideStage
.
DUEL_START
)
{
// 决斗开始,跳转
navigate
(
"
/duel
"
);
}
},
[
stage
]);
useEffect
(()
=>
{
if
(
errorMsg
!==
undefined
&&
errorMsg
!==
""
)
{
message
.
error
(
errorMsg
);
roomStore
.
errorMsg
=
undefined
;
}
},
[
errorMsg
]);
return
(
<
DndProvider
backend=
{
HTML5Backend
}
>
<
Background
/>
<
div
className=
{
styles
.
container
}
>
<
div
className=
{
styles
.
sider
}
>
<
Chat
/>
</
div
>
<
div
className=
{
styles
.
content
}
>
<
div
className=
{
styles
[
"
deck-container
"
]
}
>
<
Space
className=
{
styles
.
title
}
>
<
div
>
请拖动更换副卡组
</
div
>
<
Space
style=
{
{
marginRight
:
6
}
}
>
<
Button
type=
"text"
size=
"small"
icon=
{
<
UndoOutlined
/>
}
onClick=
{
onReset
}
>
重置
</
Button
>
<
Button
type=
"primary"
size=
"small"
icon=
{
<
CheckOutlined
/>
}
disabled=
{
stage
>
SideStage
.
SIDE_CHANGING
}
onClick=
{
onSummit
}
>
确定
</
Button
>
</
Space
>
</
Space
>
<
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=
{
canAdd
}
onChange=
{
onChange
}
onElementClick=
{
(
card
)
=>
setSelectedCard
(
card
.
id
)
}
/>
))
}
</
ScrollableArea
>
</
div
>
</
div
>
<
div
className=
{
styles
[
"
detail-container
"
]
}
>
<
CardDetail
code=
{
selectedCard
}
open=
{
true
}
onClose=
{
()
=>
{}
}
/>
</
div
>
</
div
>
<
TpModal
/>
</
DndProvider
>
);
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment