1027 lines
24 KiB
Markdown
1027 lines
24 KiB
Markdown
# 前端技术架构详设文档
|
||
|
||
> **文档版本**: v1.0
|
||
> **撰写人**: 前端架构师
|
||
> **创建日期**: 2024年9月11日
|
||
|
||
## 1. 前端架构总览
|
||
|
||
### 1.1 技术栈详细说明
|
||
|
||
```typescript
|
||
// 核心技术栈配置
|
||
{
|
||
"framework": "Taro 4.x", // 跨端开发框架
|
||
"ui": "React 18.2.0", // UI框架
|
||
"language": "TypeScript 5.0+", // 开发语言
|
||
"state": "Zustand 4.4.0", // 状态管理
|
||
"css": "Sass + CSS Modules", // 样式方案
|
||
"build": "Webpack 5 + SWC", // 构建工具
|
||
"lint": "ESLint + Prettier", // 代码规范
|
||
"test": "Jest + Testing Library" // 测试框架
|
||
}
|
||
```
|
||
|
||
### 1.2 项目目录结构
|
||
|
||
```
|
||
src/
|
||
├── components/ # 通用组件
|
||
│ ├── GameBoard/ # 游戏棋盘组件
|
||
│ ├── PlaneShape/ # 飞机形状组件
|
||
│ ├── Modal/ # 模态框组件
|
||
│ └── Loading/ # 加载组件
|
||
├── pages/ # 页面组件
|
||
│ ├── entry/ # 入口页面
|
||
│ ├── room/ # 房间相关页面
|
||
│ └── game/ # 游戏页面
|
||
├── hooks/ # 自定义Hooks
|
||
│ ├── useGame.ts # 游戏逻辑Hook
|
||
│ ├── useWebSocket.ts # WebSocket Hook
|
||
│ └── useAuth.ts # 认证Hook
|
||
├── store/ # 状态管理
|
||
│ ├── gameStore.ts # 游戏状态
|
||
│ ├── userStore.ts # 用户状态
|
||
│ └── roomStore.ts # 房间状态
|
||
├── services/ # 业务服务
|
||
│ ├── api.ts # API封装
|
||
│ ├── websocket.ts # WebSocket服务
|
||
│ └── storage.ts # 本地存储
|
||
├── utils/ # 工具函数
|
||
│ ├── gameLogic.ts # 游戏逻辑工具
|
||
│ ├── helpers.ts # 通用工具
|
||
│ └── constants.ts # 常量定义
|
||
└── styles/ # 样式文件
|
||
├── variables.scss # 变量定义
|
||
├── mixins.scss # 混合器
|
||
└── global.scss # 全局样式
|
||
```
|
||
|
||
## 2. 状态管理架构
|
||
|
||
### 2.1 Zustand Store 设计
|
||
|
||
```typescript
|
||
// 用户状态Store
|
||
interface UserState {
|
||
user: User | null
|
||
isAuthenticated: boolean
|
||
login: (userInfo: WxUserInfo) => Promise<void>
|
||
logout: () => void
|
||
updateProfile: (profile: Partial<User>) => void
|
||
}
|
||
|
||
export const useUserStore = create<UserState>((set, get) => ({
|
||
user: null,
|
||
isAuthenticated: false,
|
||
|
||
login: async (userInfo: WxUserInfo) => {
|
||
try {
|
||
const user = await apiService.login(userInfo)
|
||
set({ user, isAuthenticated: true })
|
||
// 持久化存储
|
||
await storage.setUser(user)
|
||
} catch (error) {
|
||
throw new Error('登录失败')
|
||
}
|
||
},
|
||
|
||
logout: () => {
|
||
set({ user: null, isAuthenticated: false })
|
||
storage.clearUser()
|
||
},
|
||
|
||
updateProfile: (profile: Partial<User>) => {
|
||
const currentUser = get().user
|
||
if (currentUser) {
|
||
set({ user: { ...currentUser, ...profile } })
|
||
}
|
||
}
|
||
}))
|
||
|
||
// 游戏状态Store
|
||
interface GameState {
|
||
// 当前游戏状态
|
||
currentGame: GameSession | null
|
||
gamePhase: GamePhase
|
||
currentPlayer: string
|
||
myBoard: GameBoard
|
||
opponentBoard: GameBoard
|
||
|
||
// 操作方法
|
||
initGame: (gameData: GameInitData) => void
|
||
placePlane: (plane: PlaneData) => boolean
|
||
attack: (position: Position) => void
|
||
updateGameState: (state: GameStateUpdate) => void
|
||
resetGame: () => void
|
||
}
|
||
|
||
export const useGameStore = create<GameState>((set, get) => ({
|
||
currentGame: null,
|
||
gamePhase: GamePhase.WAITING,
|
||
currentPlayer: '',
|
||
myBoard: createEmptyBoard(),
|
||
opponentBoard: createEmptyBoard(),
|
||
|
||
initGame: (gameData: GameInitData) => {
|
||
set({
|
||
currentGame: gameData.session,
|
||
gamePhase: GamePhase.PLACING,
|
||
currentPlayer: gameData.firstPlayer,
|
||
myBoard: createEmptyBoard(),
|
||
opponentBoard: createEmptyBoard()
|
||
})
|
||
},
|
||
|
||
placePlane: (plane: PlaneData): boolean => {
|
||
const state = get()
|
||
const newBoard = GameLogic.placePlane(state.myBoard, plane)
|
||
|
||
if (newBoard) {
|
||
set({ myBoard: newBoard })
|
||
return true
|
||
}
|
||
return false
|
||
},
|
||
|
||
attack: (position: Position) => {
|
||
const state = get()
|
||
if (state.gamePhase !== GamePhase.BATTLING) return
|
||
|
||
// 发送攻击请求
|
||
websocketService.sendAttack(position)
|
||
},
|
||
|
||
updateGameState: (update: GameStateUpdate) => {
|
||
set(state => ({
|
||
...state,
|
||
...update
|
||
}))
|
||
},
|
||
|
||
resetGame: () => {
|
||
set({
|
||
currentGame: null,
|
||
gamePhase: GamePhase.WAITING,
|
||
currentPlayer: '',
|
||
myBoard: createEmptyBoard(),
|
||
opponentBoard: createEmptyBoard()
|
||
})
|
||
}
|
||
}))
|
||
|
||
// 房间状态Store
|
||
interface RoomState {
|
||
currentRoom: Room | null
|
||
availableRooms: Room[]
|
||
connectionStatus: ConnectionStatus
|
||
|
||
createRoom: () => Promise<Room>
|
||
joinRoom: (roomCode: string) => Promise<boolean>
|
||
leaveRoom: () => void
|
||
refreshRoomList: () => Promise<void>
|
||
}
|
||
|
||
export const useRoomStore = create<RoomState>((set, get) => ({
|
||
currentRoom: null,
|
||
availableRooms: [],
|
||
connectionStatus: ConnectionStatus.DISCONNECTED,
|
||
|
||
createRoom: async (): Promise<Room> => {
|
||
const room = await apiService.createRoom()
|
||
set({ currentRoom: room })
|
||
// 连接WebSocket
|
||
await websocketService.joinRoom(room.code)
|
||
return room
|
||
},
|
||
|
||
joinRoom: async (roomCode: string): Promise<boolean> => {
|
||
try {
|
||
const room = await apiService.joinRoom(roomCode)
|
||
set({ currentRoom: room })
|
||
await websocketService.joinRoom(roomCode)
|
||
return true
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
},
|
||
|
||
leaveRoom: () => {
|
||
const room = get().currentRoom
|
||
if (room) {
|
||
websocketService.leaveRoom(room.code)
|
||
set({ currentRoom: null })
|
||
}
|
||
},
|
||
|
||
refreshRoomList: async () => {
|
||
const rooms = await apiService.getRoomList()
|
||
set({ availableRooms: rooms })
|
||
}
|
||
}))
|
||
```
|
||
|
||
### 2.2 状态持久化策略
|
||
|
||
```typescript
|
||
// 状态持久化中间件
|
||
const persistMiddleware = <T>(
|
||
config: StateCreator<T>,
|
||
options: {
|
||
name: string
|
||
storage: Storage
|
||
partialize?: (state: T) => Partial<T>
|
||
}
|
||
) => (set: any, get: any, api: any) =>
|
||
config(
|
||
(partial, replace) => {
|
||
const nextState = typeof partial === 'function' ? partial(get()) : partial
|
||
const stateToStore = options.partialize ? options.partialize(nextState) : nextState
|
||
|
||
// 更新状态
|
||
set(partial, replace)
|
||
|
||
// 持久化存储
|
||
options.storage.setItem(options.name, JSON.stringify(stateToStore))
|
||
},
|
||
get,
|
||
api
|
||
)
|
||
|
||
// 使用示例
|
||
export const useUserStore = create<UserState>()(
|
||
persistMiddleware(
|
||
(set, get) => ({
|
||
// 状态定义...
|
||
}),
|
||
{
|
||
name: 'user-store',
|
||
storage: wx.getStorageSync ? wxStorage : localStorage,
|
||
partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated })
|
||
}
|
||
)
|
||
)
|
||
```
|
||
|
||
## 3. 组件设计架构
|
||
|
||
### 3.1 基础组件设计
|
||
|
||
```typescript
|
||
// 游戏棋盘组件
|
||
interface GameBoardProps {
|
||
board: GameBoard
|
||
mode: 'placement' | 'battle' | 'readonly'
|
||
onCellClick?: (position: Position) => void
|
||
onPlanePlace?: (plane: PlaneData) => void
|
||
showAttackResults?: boolean
|
||
className?: string
|
||
}
|
||
|
||
export const GameBoard: React.FC<GameBoardProps> = ({
|
||
board,
|
||
mode,
|
||
onCellClick,
|
||
onPlanePlace,
|
||
showAttackResults = false,
|
||
className = ''
|
||
}) => {
|
||
const [selectedCell, setSelectedCell] = useState<Position | null>(null)
|
||
const [dragState, setDragState] = useState<DragState | null>(null)
|
||
|
||
// 处理单元格点击
|
||
const handleCellClick = useCallback((position: Position) => {
|
||
if (mode === 'readonly') return
|
||
|
||
if (mode === 'placement') {
|
||
handlePlacementClick(position)
|
||
} else if (mode === 'battle') {
|
||
handleBattleClick(position)
|
||
}
|
||
}, [mode, onCellClick, onPlanePlace])
|
||
|
||
// 处理飞机放置
|
||
const handlePlacementClick = (position: Position) => {
|
||
// 飞机放置逻辑
|
||
if (onPlanePlace) {
|
||
const plane = createPlaneAt(position, currentDirection)
|
||
onPlanePlace(plane)
|
||
}
|
||
}
|
||
|
||
// 处理攻击点击
|
||
const handleBattleClick = (position: Position) => {
|
||
if (onCellClick) {
|
||
onCellClick(position)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className={`game-board ${className} ${mode}`}>
|
||
{board.cells.map((row, x) =>
|
||
row.map((cell, y) => (
|
||
<BoardCell
|
||
key={`${x}-${y}`}
|
||
cell={cell}
|
||
position={{ x, y }}
|
||
isSelected={isSelected(position)}
|
||
onClick={handleCellClick}
|
||
showAttackResult={showAttackResults}
|
||
/>
|
||
))
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 棋盘单元格组件
|
||
interface BoardCellProps {
|
||
cell: BoardCell
|
||
position: Position
|
||
isSelected: boolean
|
||
onClick: (position: Position) => void
|
||
showAttackResult: boolean
|
||
}
|
||
|
||
const BoardCell: React.FC<BoardCellProps> = ({
|
||
cell,
|
||
position,
|
||
isSelected,
|
||
onClick,
|
||
showAttackResult
|
||
}) => {
|
||
const handleClick = () => onClick(position)
|
||
|
||
const cellClass = [
|
||
'board-cell',
|
||
cell.state,
|
||
isSelected && 'selected',
|
||
cell.planeId && 'has-plane'
|
||
].filter(Boolean).join(' ')
|
||
|
||
return (
|
||
<div
|
||
className={cellClass}
|
||
onClick={handleClick}
|
||
data-position={`${position.x}-${position.y}`}
|
||
>
|
||
{showAttackResult && cell.attackResult && (
|
||
<div className={`attack-result ${cell.attackResult.type}`}>
|
||
{cell.attackResult.type === 'HIT' ? '🎯' : '💥'}
|
||
</div>
|
||
)}
|
||
{cell.planeId && (
|
||
<div className="plane-indicator" />
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 3.2 页面组件架构
|
||
|
||
```typescript
|
||
// 游戏对战页面
|
||
const BattlePage: React.FC = () => {
|
||
const gameState = useGameStore()
|
||
const userState = useUserStore()
|
||
const { connectWebSocket, sendMessage } = useWebSocket()
|
||
|
||
// 游戏初始化
|
||
useEffect(() => {
|
||
initializeGame()
|
||
}, [])
|
||
|
||
// WebSocket消息处理
|
||
useEffect(() => {
|
||
const handleMessage = (message: GameMessage) => {
|
||
switch (message.type) {
|
||
case 'GAME_STATE_UPDATE':
|
||
gameState.updateGameState(message.data)
|
||
break
|
||
case 'ATTACK_RESULT':
|
||
handleAttackResult(message.data)
|
||
break
|
||
case 'PLAYER_DISCONNECTED':
|
||
handlePlayerDisconnect(message.data)
|
||
break
|
||
}
|
||
}
|
||
|
||
connectWebSocket(handleMessage)
|
||
}, [])
|
||
|
||
const initializeGame = async () => {
|
||
try {
|
||
const gameData = await apiService.getGameState(gameState.currentGame?.id)
|
||
gameState.initGame(gameData)
|
||
} catch (error) {
|
||
showError('游戏初始化失败')
|
||
}
|
||
}
|
||
|
||
const handleAttack = (position: Position) => {
|
||
gameState.attack(position)
|
||
}
|
||
|
||
const handleAttackResult = (result: AttackResult) => {
|
||
gameState.updateGameState({
|
||
opponentBoard: applyAttackResult(gameState.opponentBoard, result)
|
||
})
|
||
|
||
if (result.gameEnded) {
|
||
showGameResult(result.winner)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="battle-page">
|
||
<GameHeader
|
||
currentPlayer={gameState.currentPlayer}
|
||
gamePhase={gameState.gamePhase}
|
||
/>
|
||
|
||
<div className="battle-boards">
|
||
<div className="my-board-section">
|
||
<h3>我的棋盘</h3>
|
||
<GameBoard
|
||
board={gameState.myBoard}
|
||
mode="readonly"
|
||
showAttackResults={true}
|
||
/>
|
||
</div>
|
||
|
||
<div className="opponent-board-section">
|
||
<h3>对手棋盘</h3>
|
||
<GameBoard
|
||
board={gameState.opponentBoard}
|
||
mode="battle"
|
||
onCellClick={handleAttack}
|
||
showAttackResults={true}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<GameControls
|
||
gamePhase={gameState.gamePhase}
|
||
currentPlayer={gameState.currentPlayer}
|
||
onSurrender={() => gameState.surrender()}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## 4. 自定义Hooks设计
|
||
|
||
### 4.1 游戏逻辑Hooks
|
||
|
||
```typescript
|
||
// 游戏逻辑Hook
|
||
export const useGame = () => {
|
||
const gameStore = useGameStore()
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
// 开始游戏
|
||
const startGame = async (roomCode: string) => {
|
||
setLoading(true)
|
||
setError(null)
|
||
|
||
try {
|
||
const gameData = await apiService.startGame(roomCode)
|
||
gameStore.initGame(gameData)
|
||
} catch (err) {
|
||
setError('游戏启动失败')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 放置飞机
|
||
const placePlane = (plane: PlaneData) => {
|
||
const success = gameStore.placePlane(plane)
|
||
if (!success) {
|
||
setError('飞机放置失败,请检查位置')
|
||
return false
|
||
}
|
||
|
||
// 如果已放置3架飞机,自动进入准备状态
|
||
if (gameStore.myBoard.planes.length === 3) {
|
||
confirmPlacement()
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// 确认飞机布置
|
||
const confirmPlacement = async () => {
|
||
try {
|
||
await apiService.confirmPlacement(gameStore.myBoard.planes)
|
||
gameStore.updateGameState({ gamePhase: GamePhase.WAITING_OPPONENT })
|
||
} catch (err) {
|
||
setError('确认布置失败')
|
||
}
|
||
}
|
||
|
||
// 执行攻击
|
||
const attack = async (position: Position) => {
|
||
if (gameStore.gamePhase !== GamePhase.BATTLING) return
|
||
|
||
try {
|
||
const result = await apiService.attack(position)
|
||
gameStore.updateGameState({
|
||
opponentBoard: applyAttackResult(gameStore.opponentBoard, result)
|
||
})
|
||
|
||
if (result.gameEnded) {
|
||
gameStore.updateGameState({ gamePhase: GamePhase.FINISHED })
|
||
}
|
||
} catch (err) {
|
||
setError('攻击失败')
|
||
}
|
||
}
|
||
|
||
return {
|
||
...gameStore,
|
||
loading,
|
||
error,
|
||
startGame,
|
||
placePlane,
|
||
confirmPlacement,
|
||
attack,
|
||
clearError: () => setError(null)
|
||
}
|
||
}
|
||
|
||
// WebSocket Hook
|
||
export const useWebSocket = () => {
|
||
const [socket, setSocket] = useState<WebSocketConnection | null>(null)
|
||
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected')
|
||
const [lastMessage, setLastMessage] = useState<GameMessage | null>(null)
|
||
|
||
// 连接WebSocket
|
||
const connect = useCallback((onMessage: (message: GameMessage) => void) => {
|
||
if (socket?.readyState === WebSocket.OPEN) return
|
||
|
||
const ws = new WebSocketConnection({
|
||
url: WS_URL,
|
||
onOpen: () => setConnectionState('connected'),
|
||
onClose: () => setConnectionState('disconnected'),
|
||
onError: () => setConnectionState('error'),
|
||
onMessage: (message) => {
|
||
setLastMessage(message)
|
||
onMessage(message)
|
||
}
|
||
})
|
||
|
||
setSocket(ws)
|
||
setConnectionState('connecting')
|
||
}, [socket])
|
||
|
||
// 发送消息
|
||
const sendMessage = useCallback((message: GameMessage) => {
|
||
if (socket?.readyState === WebSocket.OPEN) {
|
||
socket.send(JSON.stringify(message))
|
||
}
|
||
}, [socket])
|
||
|
||
// 断线重连
|
||
useEffect(() => {
|
||
let reconnectTimer: NodeJS.Timeout
|
||
|
||
if (connectionState === 'disconnected') {
|
||
reconnectTimer = setTimeout(() => {
|
||
if (socket) {
|
||
connect(() => {}) // 重连
|
||
}
|
||
}, 3000)
|
||
}
|
||
|
||
return () => clearTimeout(reconnectTimer)
|
||
}, [connectionState, connect])
|
||
|
||
// 清理连接
|
||
useEffect(() => {
|
||
return () => {
|
||
if (socket) {
|
||
socket.close()
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
return {
|
||
connectionState,
|
||
lastMessage,
|
||
connect,
|
||
sendMessage,
|
||
disconnect: () => socket?.close()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 UI交互Hooks
|
||
|
||
```typescript
|
||
// 模态框Hook
|
||
export const useModal = () => {
|
||
const [isOpen, setIsOpen] = useState(false)
|
||
const [title, setTitle] = useState('')
|
||
const [content, setContent] = useState<React.ReactNode>(null)
|
||
|
||
const openModal = (modalTitle: string, modalContent: React.ReactNode) => {
|
||
setTitle(modalTitle)
|
||
setContent(modalContent)
|
||
setIsOpen(true)
|
||
}
|
||
|
||
const closeModal = () => {
|
||
setIsOpen(false)
|
||
}
|
||
|
||
const Modal = ({ children }: { children?: React.ReactNode }) => {
|
||
if (!isOpen) return null
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={closeModal}>
|
||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-header">
|
||
<h3>{title}</h3>
|
||
<button onClick={closeModal}>×</button>
|
||
</div>
|
||
<div className="modal-body">
|
||
{content || children}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return {
|
||
isOpen,
|
||
openModal,
|
||
closeModal,
|
||
Modal
|
||
}
|
||
}
|
||
|
||
// 加载状态Hook
|
||
export const useLoading = () => {
|
||
const [loading, setLoading] = useState(false)
|
||
const [loadingText, setLoadingText] = useState('加载中...')
|
||
|
||
const showLoading = (text = '加载中...') => {
|
||
setLoadingText(text)
|
||
setLoading(true)
|
||
}
|
||
|
||
const hideLoading = () => {
|
||
setLoading(false)
|
||
}
|
||
|
||
const LoadingComponent = () => {
|
||
if (!loading) return null
|
||
|
||
return (
|
||
<div className="loading-overlay">
|
||
<div className="loading-spinner">
|
||
<div className="spinner"></div>
|
||
<p>{loadingText}</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return {
|
||
loading,
|
||
showLoading,
|
||
hideLoading,
|
||
LoadingComponent
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. 样式架构设计
|
||
|
||
### 5.1 设计Token系统
|
||
|
||
```scss
|
||
// styles/variables.scss - 设计变量定义
|
||
:root {
|
||
// 颜色系统 - 基于原型的深色科技主题
|
||
--color-primary: #6366f1;
|
||
--color-secondary: #40e0d0;
|
||
--color-accent: #ff6b6b;
|
||
--color-success: #51cf66;
|
||
--color-warning: #ffd43b;
|
||
--color-danger: #ff6b6b;
|
||
|
||
// 背景色
|
||
--bg-primary: #0f1419;
|
||
--bg-secondary: #1a1a2e;
|
||
--bg-card: #16213e;
|
||
--bg-overlay: rgba(15, 20, 25, 0.9);
|
||
|
||
// 渐变背景
|
||
--gradient-primary: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 30%, #16213e 70%, #0f3460 100%);
|
||
--gradient-card: linear-gradient(145deg, #1e2a47 0%, #2a3f5f 100%);
|
||
|
||
// 文字颜色
|
||
--text-primary: #ffffff;
|
||
--text-secondary: #b8c5d3;
|
||
--text-muted: #6c7983;
|
||
--text-disabled: #4a5568;
|
||
|
||
// 边框和分割线
|
||
--border-color: #2d3748;
|
||
--border-hover: #4a5568;
|
||
--divider: #2d3748;
|
||
|
||
// 阴影
|
||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.16);
|
||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.2);
|
||
--shadow-glow: 0 0 20px rgba(99, 102, 241, 0.3);
|
||
|
||
// 间距系统
|
||
--space-xs: 4px;
|
||
--space-sm: 8px;
|
||
--space-md: 16px;
|
||
--space-lg: 24px;
|
||
--space-xl: 32px;
|
||
--space-xxl: 48px;
|
||
|
||
// 字体大小
|
||
--font-xs: 12px;
|
||
--font-sm: 14px;
|
||
--font-base: 16px;
|
||
--font-lg: 18px;
|
||
--font-xl: 20px;
|
||
--font-2xl: 24px;
|
||
--font-3xl: 30px;
|
||
|
||
// 圆角
|
||
--radius-sm: 4px;
|
||
--radius-md: 8px;
|
||
--radius-lg: 12px;
|
||
--radius-xl: 16px;
|
||
--radius-full: 50%;
|
||
|
||
// Z-index层级
|
||
--z-modal: 1000;
|
||
--z-loading: 999;
|
||
--z-toast: 998;
|
||
--z-header: 100;
|
||
}
|
||
```
|
||
|
||
### 5.2 组件样式库
|
||
|
||
```scss
|
||
// styles/components.scss - 组件样式
|
||
.game-board {
|
||
display: grid;
|
||
grid-template-columns: repeat(10, 1fr);
|
||
grid-template-rows: repeat(10, 1fr);
|
||
gap: 2px;
|
||
padding: var(--space-md);
|
||
background: var(--gradient-card);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-lg);
|
||
|
||
&.placement {
|
||
.board-cell {
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background: var(--color-primary);
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
}
|
||
|
||
&.battle {
|
||
.board-cell {
|
||
cursor: crosshair;
|
||
|
||
&:hover:not(.attacked) {
|
||
background: var(--color-accent);
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.board-cell {
|
||
aspect-ratio: 1;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-sm);
|
||
position: relative;
|
||
transition: all 0.2s ease;
|
||
|
||
&.has-plane {
|
||
background: var(--color-secondary);
|
||
}
|
||
|
||
&.attacked-miss {
|
||
background: var(--bg-card);
|
||
|
||
&::after {
|
||
content: '○';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
color: var(--text-muted);
|
||
font-size: var(--font-sm);
|
||
}
|
||
}
|
||
|
||
&.attacked-hit {
|
||
background: var(--color-danger);
|
||
|
||
&::after {
|
||
content: '✕';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
color: var(--text-primary);
|
||
font-size: var(--font-base);
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
&.selected {
|
||
background: var(--color-primary);
|
||
box-shadow: var(--shadow-glow);
|
||
}
|
||
}
|
||
|
||
.attack-result {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: var(--font-lg);
|
||
animation: attackResult 0.8s ease-out;
|
||
|
||
&.HIT {
|
||
color: var(--color-success);
|
||
}
|
||
|
||
&.MISS {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
&.DESTROY {
|
||
color: var(--color-danger);
|
||
font-size: var(--font-xl);
|
||
}
|
||
}
|
||
|
||
@keyframes attackResult {
|
||
0% {
|
||
transform: translate(-50%, -50%) scale(0);
|
||
opacity: 0;
|
||
}
|
||
50% {
|
||
transform: translate(-50%, -50%) scale(1.5);
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
transform: translate(-50%, -50%) scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. 性能优化策略
|
||
|
||
### 6.1 渲染优化
|
||
|
||
```typescript
|
||
// 使用React.memo优化组件渲染
|
||
export const GameBoard = React.memo<GameBoardProps>(({
|
||
board,
|
||
mode,
|
||
onCellClick,
|
||
onPlanePlace
|
||
}) => {
|
||
// 只有相关props变化时才重渲染
|
||
}, (prevProps, nextProps) => {
|
||
return (
|
||
prevProps.board === nextProps.board &&
|
||
prevProps.mode === nextProps.mode &&
|
||
prevProps.onCellClick === nextProps.onCellClick &&
|
||
prevProps.onPlanePlace === nextProps.onPlanePlace
|
||
)
|
||
})
|
||
|
||
// 使用useMemo缓存计算结果
|
||
const BoardCell = React.memo<BoardCellProps>(({ cell, position, onClick }) => {
|
||
const cellClass = useMemo(() => {
|
||
return [
|
||
'board-cell',
|
||
cell.state,
|
||
cell.planeId && 'has-plane'
|
||
].filter(Boolean).join(' ')
|
||
}, [cell.state, cell.planeId])
|
||
|
||
const handleClick = useCallback(() => {
|
||
onClick(position)
|
||
}, [onClick, position])
|
||
|
||
return (
|
||
<div className={cellClass} onClick={handleClick}>
|
||
{/* 单元格内容 */}
|
||
</div>
|
||
)
|
||
})
|
||
```
|
||
|
||
### 6.2 状态更新优化
|
||
|
||
```typescript
|
||
// 批量状态更新
|
||
class GameStateManager {
|
||
private updateQueue: StateUpdate[] = []
|
||
private isUpdating = false
|
||
|
||
queueUpdate(update: StateUpdate) {
|
||
this.updateQueue.push(update)
|
||
if (!this.isUpdating) {
|
||
this.flushUpdates()
|
||
}
|
||
}
|
||
|
||
private async flushUpdates() {
|
||
this.isUpdating = true
|
||
|
||
// 合并所有更新
|
||
const mergedUpdate = this.updateQueue.reduce((merged, update) => {
|
||
return { ...merged, ...update }
|
||
}, {})
|
||
|
||
// 批量应用更新
|
||
useGameStore.setState(state => ({
|
||
...state,
|
||
...mergedUpdate
|
||
}))
|
||
|
||
this.updateQueue = []
|
||
this.isUpdating = false
|
||
}
|
||
}
|
||
```
|
||
|
||
## 7. 错误处理机制
|
||
|
||
### 7.1 错误边界设计
|
||
|
||
```typescript
|
||
// 游戏错误边界
|
||
class GameErrorBoundary extends React.Component<
|
||
{ children: React.ReactNode },
|
||
{ hasError: boolean; error: Error | null }
|
||
> {
|
||
constructor(props: any) {
|
||
super(props)
|
||
this.state = { hasError: false, error: null }
|
||
}
|
||
|
||
static getDerivedStateFromError(error: Error) {
|
||
return { hasError: true, error }
|
||
}
|
||
|
||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||
console.error('游戏错误:', error, errorInfo)
|
||
|
||
// 上报错误
|
||
this.reportError(error, errorInfo)
|
||
}
|
||
|
||
private reportError(error: Error, errorInfo: ErrorInfo) {
|
||
// 调用错误上报服务
|
||
errorReportingService.log({
|
||
message: error.message,
|
||
stack: error.stack,
|
||
componentStack: errorInfo.componentStack
|
||
})
|
||
}
|
||
|
||
render() {
|
||
if (this.state.hasError) {
|
||
return (
|
||
<div className="error-fallback">
|
||
<h2>游戏出错了</h2>
|
||
<p>我们已经记录了错误信息,请稍后重试。</p>
|
||
<button onClick={() => window.location.reload()}>刷新页面</button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return this.props.children
|
||
}
|
||
} |