55 <GameSetup v-if =" showSetup " @start =" handleGameSetupStart " />
66
77 <!-- 棋盘主面板 -->
8- <div class =" game-panel" >
9- <div class =" board-frame" :class =" { 'coordinates-outside': coordinateLabelMode === 'outside' }" >
10- <div class =" board-grid" ref =" boardGridRef" >
11- <template v-for =" displayRow in 8 " :key =" ` rank-${displayRow } ` " >
12- <button v-for =" displayCol in 8" :key =" `${displayRow}-${displayCol}`" type =" button" class =" board-square"
13- :class =" { 'draggable-piece': board[getActualRow(displayRow - 1)]?.[getActualCol(displayCol - 1)]?.color === currentTurn }"
14- @mousedown =" handleMouseDown(getActualRow(displayRow - 1), getActualCol(displayCol - 1), $event)"
15- @mouseenter =" hoverSquare = { row: getActualRow(displayRow - 1), col: getActualCol(displayCol - 1) }"
16- @mouseleave =" hoverSquare = null"
17- :aria-label =" getSquareLabel(getActualRow(displayRow - 1), getActualCol(displayCol - 1))" >
18-
19- <img class =" square-background base" draggable =" false"
20- :src =" isWhiteSquare(getActualRow(displayRow - 1), getActualCol(displayCol - 1)) ? './texture/board/board_white.png' : './texture/board/board_black.png'"
21- alt =" " />
22-
23- <img v-if =" getOverlayTexture(board, selectedSquare, possibleMoves, isDragging, hoverSquare, getActualRow(displayRow - 1), getActualCol(displayCol - 1))"
24- class =" square-background overlay" draggable =" false"
25- :src =" getOverlayTexture(board, selectedSquare, possibleMoves, isDragging, hoverSquare, getActualRow(displayRow - 1), getActualCol(displayCol - 1))!" alt =" " />
26-
27- <img v-if =" board[getActualRow(displayRow - 1)]?.[getActualCol(displayCol - 1)]" class =" piece"
28- draggable =" false" :class =" {
29- 'dragging-hidden': isDragging && dragStartSquare?.row === getActualRow(displayRow - 1) && dragStartSquare?.col === getActualCol(displayCol - 1)
30- }" :src =" getPieceImage(board[getActualRow(displayRow - 1)]?.[getActualCol(displayCol - 1)]!, board, isDraw, hasResigned, timeoutWinner)"
31- :alt =" board[getActualRow(displayRow - 1)]?.[getActualCol(displayCol - 1)]!.type" />
32-
33- <div v-if =" coordinateLabelMode === 'inside' && displayCol === 1" class =" coordinate-label rank"
34- :class =" isWhiteSquare(getActualRow(displayRow - 1), getActualCol(displayCol - 1)) ? 'text-black' : 'text-white'" >
35- {{ 8 - getActualRow(displayRow - 1) }}
36- </div >
37-
38- <div v-if =" coordinateLabelMode === 'inside' && displayRow === 8" class =" coordinate-label file"
39- :class =" isWhiteSquare(getActualRow(displayRow - 1), getActualCol(displayCol - 1)) ? 'text-black' : 'text-white'" >
40- {{ String.fromCharCode(97 + getActualCol(displayCol - 1)) }}
41- </div >
42- </button >
43- </template >
44- <div v-if =" promotionPending" class =" promotion-overlay" @click =" cancelPromotion" ></div >
45- <Promotion v-if =" promotionPending " :color =" promotionPending .color " :style =" promotionStyle "
46- @select =" applyPromotion " />
47- </div >
48-
49- <div v-if =" coordinateLabelMode === 'outside'" class =" coordinates-outside-layer" >
50- <div class =" coordinate-bottom-row" >
51- <span v-for =" displayCol in 8" :key =" `file-${displayCol}`" class =" coordinate-label outer-file"
52- :style =" { left: `${(displayCol - 0.5) * 12.5}%` }" >
53- {{ getDisplayedFile(displayCol) }}
54- </span >
55- </div >
56-
57- <div class =" coordinate-side-col" >
58- <span v-for =" displayRow in 8" :key =" `rank-${displayRow}`" class =" coordinate-label outer-rank"
59- :style =" { top: `${(displayRow - 0.5) * 12.5}%` }" >
60- {{ getDisplayedRank(displayRow) }}
61- </span >
62- </div >
63- </div >
64- </div >
65-
66- <img v-if =" isDragging && dragStartSquare" class =" floating-piece no-select" draggable =" false"
67- :src =" getPieceImage(board[dragStartSquare.row]?.[dragStartSquare.col]!, board, isDraw, hasResigned, timeoutWinner)"
68- :style =" { left: mousePos.x + 'px', top: mousePos.y + 'px' }" alt =" " />
69- </div >
8+ <BoardPanel :board =" board " :current-turn =" currentTurn " :selected-square =" selectedSquare "
9+ :possible-moves =" possibleMoves " :is-dragging =" isDragging " :drag-start-square =" dragStartSquare "
10+ :hover-square =" hoverSquare " :mouse-pos =" mousePos " :is-mouse-down =" isMouseDown "
11+ :promotion-pending =" promotionPending " :promotion-style =" promotionStyle " :is-draw =" isDraw "
12+ :has-resigned =" hasResigned " :timeout-winner =" timeoutWinner " :coordinate-label-mode =" coordinateLabelMode "
13+ :is-flipped =" isFlipped "
14+ :get-overlay-texture =" getOverlayTexture "
15+ :get-piece-image =" getPieceImage "
16+ :get-square-label =" getSquareLabel "
17+ :is-white-square =" isWhiteSquare "
18+ @update :piece-scale =" (val : number ) => pieceScale = val "
19+ @square-mousedown =" (row : number , col : number , event : MouseEvent ) => handleMouseDown (row , col , event ) "
20+ @square-mouseenter =" (row : number , col : number ) => hoverSquare = { row , col } "
21+ @square-mouseleave =" hoverSquare = null "
22+ @cancel-promotion =" cancelPromotion "
23+ @apply-promotion =" (piece : string ) => applyPromotion (piece ) " />
7024
7125 <!-- 侧边栏 -->
7226 <Sidebar :is-clock-enabled =" isClockEnabled " :move-history =" moveHistory " :current-turn =" currentTurn "
9246</template >
9347
9448<script setup lang="ts">
95- import { onMounted , onUnmounted , ref } from ' vue'
96- import Promotion from ' ./components/Promotion.vue'
49+ import { onUnmounted , ref } from ' vue'
9750import Sidebar from ' ./components/Sidebar.vue'
9851import SettingsModal from ' ./components/SettingsModal.vue'
9952import GameSetup from ' ./components/GameSetup.vue'
53+ import BoardPanel from ' ./components/BoardPanel.vue'
10054import { useSettings } from ' ./composables/useSettings'
10155import { useBoardDisplay } from ' ./composables/useBoardDisplay'
10256import { useGameState } from ' ./composables/useGameState'
@@ -110,19 +64,15 @@ const showSettingsModal = ref(false)
11064// ---- 棋盘显示 ----
11165const {
11266 isFlipped,
113- getActualRow,
114- getActualCol,
115- getDisplayedFile,
116- getDisplayedRank,
11767 getOverlayTexture,
11868 getPieceImage,
119- pieceScale,
120- boardGridRef,
12169 getSquareLabel,
12270 isWhiteSquare,
123- updatePieceScale,
12471} = useBoardDisplay ()
12572
73+ // ---- pieceScale(由 BoardPanel 通过事件更新) ----
74+ const pieceScale = ref (1.5 )
75+
12676// ---- 游戏核心状态(含棋钟、拖拽、音效、走棋) ----
12777const game = useGameState (isSoundEnabled , isFlipped )
12878
@@ -166,10 +116,6 @@ const {
166116} = game
167117
168118// ---- 生命周期 ----
169- onMounted (() => {
170- updatePieceScale ()
171- })
172-
173119onUnmounted (() => {
174120 stopClock ()
175121})
@@ -194,195 +140,11 @@ onUnmounted(() => {
194140 cursor : auto ;
195141}
196142
197- .game-panel {
198- width : 100% ;
199- max-width : 80vmin ;
200- display : flex ;
201- flex-direction : column ;
202- justify-content : center ;
203- align-items : center ;
204- box-sizing : border-box ;
205- }
206-
207143.game-container.global-dragging ,
208144.game-container.global-dragging * {
209145 cursor : grabbing !important ;
210146}
211147
212- .board-frame {
213- position : relative ;
214- width : 100% ;
215- aspect-ratio : 1 / 1 ;
216- max-width : calc (100vh - 120px );
217- width : 80vmin ;
218- margin : 0 auto ;
219- box-sizing : border-box ;
220- overflow : visible ;
221- }
222-
223- .board-frame.coordinates-outside {
224- padding : 0 ;
225- }
226-
227- .board-grid {
228- display : grid ;
229- grid-template-columns : repeat (8 , minmax (0 , 1fr ));
230- grid-template-rows : repeat (8 , minmax (0 , 1fr ));
231- width : 100% ;
232- height : 100% ;
233- gap : 0 ;
234- position : relative ;
235- }
236-
237- .board-square {
238- position : relative ;
239- overflow : visible ;
240- border : none ;
241- padding : 0 ;
242- background : transparent ;
243- cursor : default ;
244- }
245-
246- .board-square :focus ,
247- .board-square :focus-visible ,
248- .board-square :active {
249- outline : none !important ;
250- box-shadow : none !important ;
251- }
252-
253- .board-square.draggable-piece {
254- cursor : grab ;
255- }
256-
257- .board-square :focus-visible {
258- outline : 2px solid #ffd33d ;
259- outline-offset : -2px ;
260- z-index : 10 ;
261- }
262-
263- .square-background ,
264- .piece {
265- display : block ;
266- image-rendering : crisp-edges ;
267- image-rendering : pixelated ;
268- }
269-
270- .square-background.base {
271- width : 100% ;
272- height : 100% ;
273- object-fit : cover ;
274- }
275-
276- .square-background.overlay {
277- position : absolute ;
278- top : 0 ;
279- left : 0 ;
280- width : 100% ;
281- height : 100% ;
282- z-index : 1 ;
283- pointer-events : none ;
284- }
285-
286- .piece {
287- position : absolute ;
288- left : 50% ;
289- bottom : 15% ;
290- transform : translateX (-50% ) scale (var (--piece-scale ));
291- transform-origin : bottom center ;
292- pointer-events : auto ;
293- cursor : grab ;
294- z-index : 10 ;
295- transition : opacity 0.1s ease ;
296- }
297-
298- .piece.dragging-hidden {
299- opacity : 0 ;
300- }
301-
302- .floating-piece {
303- position : fixed ;
304- pointer-events : none ;
305- z-index : 9999 ;
306- transform : translate (-50% , -50% ) scale (var (--piece-scale ));
307- image-rendering : crisp-edges ;
308- image-rendering : pixelated ;
309- }
310-
311- .coordinate-label {
312- position : absolute ;
313- font-family : ' Unifont' , monospace ;
314- font-size : 0.75rem ;
315- line-height : 1 ;
316- font-weight : bold ;
317- pointer-events : none ;
318- z-index : 5 ;
319- user-select : none ;
320- }
321-
322- .coordinates-outside-layer {
323- position : absolute ;
324- inset : 0 ;
325- pointer-events : none ;
326- }
327-
328- .coordinate-bottom-row {
329- position : absolute ;
330- left : 0 ;
331- right : 0 ;
332- bottom : -1.25rem ;
333- height : 1rem ;
334- }
335-
336- .coordinate-side-col {
337- position : absolute ;
338- top : 0 ;
339- bottom : 0 ;
340- left : -1.25rem ;
341- width : 1rem ;
342- }
343-
344- .coordinate-label.outer-file {
345- position : absolute ;
346- transform : translateX (-50% );
347- font-size : 0.85rem ;
348- color : #333 ;
349- text-align : center ;
350- }
351-
352- .coordinate-label.outer-rank {
353- position : absolute ;
354- transform : translateY (-50% );
355- font-size : 0.85rem ;
356- color : #333 ;
357- text-align : center ;
358- width : 100% ;
359- }
360-
361- .coordinate-label.rank {
362- top : 2px ;
363- left : 4px ;
364- }
365-
366- .coordinate-label.file {
367- bottom : 3px ;
368- right : 4px ;
369- }
370-
371- .coordinate-label.text-black {
372- color : #484A4B ;
373- }
374-
375- .coordinate-label.text-white {
376- color : #F3F9FC ;
377- }
378-
379- .promotion-overlay {
380- position : absolute ;
381- inset : 0 ;
382- background : rgba (0 , 0 , 0 , 0.45 );
383- z-index : 50 ;
384- }
385-
386148/* 右上角固定设置按钮 */
387149.settings-fab {
388150 position : fixed ;
0 commit comments