Initial commit

This commit is contained in:
史悦
2025-09-10 18:13:28 +08:00
commit 40f3e2dedb
24 changed files with 14886 additions and 0 deletions

View File

@@ -0,0 +1,819 @@
# 打飞机小程序 WebSocket 接口规范与断线重连策略
## 1. WebSocket 连接规范
### 1.1 连接地址
```
wss://your-domain.com/game-hub?token={authToken}&gameId={gameId}
```
### 1.2 .NET Core WebSocket Hub 实现参考
```csharp
[Authorize]
public class GameHub : Hub
{
private readonly IGameService _gameService;
private readonly IConnectionManager _connectionManager;
public GameHub(IGameService gameService, IConnectionManager connectionManager)
{
_gameService = gameService;
_connectionManager = connectionManager;
}
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier;
var gameId = Context.GetHttpContext().Request.Query["gameId"];
await Groups.AddToGroupAsync(Context.ConnectionId, $"Game_{gameId}");
await _connectionManager.RegisterConnection(userId, Context.ConnectionId, gameId);
// 发送连接成功确认
await Clients.Caller.SendAsync("ConnectionConfirmed", new
{
ConnectionId = Context.ConnectionId,
Timestamp = DateTime.UtcNow,
GameId = gameId
});
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var userId = Context.UserIdentifier;
await _connectionManager.UnregisterConnection(Context.ConnectionId);
// 通知游戏中的其他玩家
await Clients.Others.SendAsync("PlayerDisconnected", new
{
UserId = userId,
Timestamp = DateTime.UtcNow,
Reason = exception?.Message
});
await base.OnDisconnectedAsync(exception);
}
}
```
## 2. 消息协议定义
### 2.1 基础消息格式
```typescript
interface WebSocketMessage {
messageId: string // 消息唯一ID用于确认和重发
type: MessageType // 消息类型
timestamp: number // 时间戳
gameId: string // 游戏ID
userId: string // 发送者ID
data: any // 消息内容
sequenceNumber: number // 序列号,用于消息排序
requiresAck?: boolean // 是否需要确认回复
}
enum MessageType {
// 连接管理
PING = 'PING',
PONG = 'PONG',
HEARTBEAT = 'HEARTBEAT',
CONNECTION_CONFIRMED = 'CONNECTION_CONFIRMED',
// 游戏房间
JOIN_ROOM = 'JOIN_ROOM',
LEAVE_ROOM = 'LEAVE_ROOM',
ROOM_STATE_UPDATE = 'ROOM_STATE_UPDATE',
// 游戏操作
PLACE_PLANES = 'PLACE_PLANES',
ATTACK_POSITION = 'ATTACK_POSITION',
ATTACK_RESULT = 'ATTACK_RESULT',
GAME_STATE_SYNC = 'GAME_STATE_SYNC',
// 系统消息
PLAYER_RECONNECT = 'PLAYER_RECONNECT',
PLAYER_DISCONNECT = 'PLAYER_DISCONNECT',
GAME_END = 'GAME_END',
ERROR = 'ERROR',
// 确认消息
ACK = 'ACK',
MESSAGE_RECEIVED = 'MESSAGE_RECEIVED'
}
```
### 2.2 关键消息定义
#### A. 游戏操作消息
```csharp
// .NET 消息模型
public class AttackMessage
{
public string MessageId { get; set; } = Guid.NewGuid().ToString();
public string Type { get; set; } = "ATTACK_POSITION";
public long Timestamp { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
public string GameId { get; set; }
public string UserId { get; set; }
public AttackData Data { get; set; }
public int SequenceNumber { get; set; }
public bool RequiresAck { get; set; } = true;
}
public class AttackData
{
public Position Position { get; set; }
public string PlayerId { get; set; }
}
public class Position
{
public int X { get; set; }
public int Y { get; set; }
public string Coordinate { get; set; }
}
```
#### B. 游戏状态同步
```csharp
public class GameStateSyncMessage
{
public string MessageId { get; set; } = Guid.NewGuid().ToString();
public string Type { get; set; } = "GAME_STATE_SYNC";
public long Timestamp { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
public string GameId { get; set; }
public GameStateData Data { get; set; }
public int SequenceNumber { get; set; }
}
public class GameStateData
{
public string CurrentPlayer { get; set; }
public string GamePhase { get; set; }
public List<GameMove> MoveHistory { get; set; }
public Dictionary<string, PlayerState> Players { get; set; }
public long LastUpdateTime { get; set; }
}
```
## 3. .NET WebSocket Hub 核心方法
### 3.1 游戏操作处理
```csharp
[HubMethodName("AttackPosition")]
public async Task HandleAttack(AttackMessage message)
{
try
{
// 验证消息
if (!await ValidateMessage(message))
{
await SendError("Invalid attack message", message.MessageId);
return;
}
// 处理攻击逻辑
var result = await _gameService.ProcessAttack(
message.GameId,
message.UserId,
message.Data.Position
);
// 发送攻击结果给所有玩家
var resultMessage = new AttackResultMessage
{
MessageId = Guid.NewGuid().ToString(),
GameId = message.GameId,
Data = result,
SequenceNumber = await GetNextSequenceNumber(message.GameId)
};
await Clients.Group($"Game_{message.GameId}")
.SendAsync("AttackResult", resultMessage);
// 发送确认回复
if (message.RequiresAck)
{
await Clients.Caller.SendAsync("MessageReceived", new
{
OriginalMessageId = message.MessageId,
Status = "SUCCESS",
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
}
// 检查游戏是否结束
if (result.GameEnded)
{
await HandleGameEnd(message.GameId, result.Winner);
}
}
catch (Exception ex)
{
await SendError($"Attack processing failed: {ex.Message}", message.MessageId);
}
}
[HubMethodName("PlacePlanes")]
public async Task HandlePlacePlanes(PlacePlanesMessage message)
{
try
{
var isValid = await _gameService.ValidatePlacement(
message.GameId,
message.UserId,
message.Data.Planes
);
if (!isValid.Success)
{
await SendError(isValid.ErrorMessage, message.MessageId);
return;
}
await _gameService.SavePlacement(message.GameId, message.UserId, message.Data.Planes);
// 通知房间状态更新
var roomState = await _gameService.GetRoomState(message.GameId);
await Clients.Group($"Game_{message.GameId}")
.SendAsync("RoomStateUpdate", roomState);
// 确认消息
if (message.RequiresAck)
{
await Clients.Caller.SendAsync("MessageReceived", new
{
OriginalMessageId = message.MessageId,
Status = "SUCCESS"
});
}
}
catch (Exception ex)
{
await SendError($"Plane placement failed: {ex.Message}", message.MessageId);
}
}
```
### 3.2 心跳和连接管理
```csharp
[HubMethodName("Heartbeat")]
public async Task HandleHeartbeat(HeartbeatMessage message)
{
await _connectionManager.UpdateLastActivity(Context.ConnectionId);
// 发送心跳响应
await Clients.Caller.SendAsync("HeartbeatResponse", new
{
ServerTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ConnectionId = Context.ConnectionId,
Status = "ALIVE"
});
}
[HubMethodName("RequestGameState")]
public async Task HandleGameStateRequest(string gameId)
{
var gameState = await _gameService.GetCompleteGameState(gameId);
await Clients.Caller.SendAsync("GameStateSync", new GameStateSyncMessage
{
GameId = gameId,
Data = gameState,
SequenceNumber = await GetNextSequenceNumber(gameId)
});
}
```
## 4. 客户端断线重连策略
### 4.1 连接管理器
```typescript
export class WebSocketManager {
private connection: HubConnection | null = null
private reconnectAttempts = 0
private readonly maxReconnectAttempts = 5
private readonly baseReconnectDelay = 1000
private heartbeatInterval: NodeJS.Timeout | null = null
private messageQueue: QueuedMessage[] = []
private sequenceNumber = 0
private lastReceivedSequence = 0
constructor(
private gameId: string,
private authToken: string,
private onMessage: (message: WebSocketMessage) => void,
private onConnectionStateChange: (state: ConnectionState) => void
) {}
async connect(): Promise<void> {
try {
this.connection = new HubConnectionBuilder()
.withUrl(`wss://your-domain.com/game-hub?gameId=${this.gameId}`, {
accessTokenFactory: () => this.authToken,
transport: HttpTransportType.WebSockets
})
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
// 指数退避策略
return Math.min(
this.baseReconnectDelay * Math.pow(2, retryContext.previousRetryCount),
30000
)
}
})
.configureLogging(LogLevel.Information)
.build()
// 注册消息处理器
this.setupMessageHandlers()
// 开始连接
await this.connection.start()
this.onConnectionStateChange('CONNECTED')
this.startHeartbeat()
this.reconnectAttempts = 0
// 重连后请求完整游戏状态
await this.requestGameState()
} catch (error) {
console.error('WebSocket connection failed:', error)
await this.handleConnectionError()
}
}
private setupMessageHandlers(): void {
if (!this.connection) return
// 连接确认
this.connection.on('ConnectionConfirmed', (data) => {
console.log('Connection confirmed:', data)
this.onConnectionStateChange('CONNECTED')
})
// 攻击结果
this.connection.on('AttackResult', (message: WebSocketMessage) => {
this.handleReceivedMessage(message)
})
// 游戏状态同步
this.connection.on('GameStateSync', (message: WebSocketMessage) => {
this.handleReceivedMessage(message)
this.syncSequenceNumber(message.sequenceNumber)
})
// 房间状态更新
this.connection.on('RoomStateUpdate', (message: WebSocketMessage) => {
this.handleReceivedMessage(message)
})
// 玩家断线/重连
this.connection.on('PlayerDisconnected', (data) => {
this.onMessage({
messageId: Date.now().toString(),
type: 'PLAYER_DISCONNECT',
timestamp: Date.now(),
gameId: this.gameId,
userId: data.userId,
data,
sequenceNumber: 0
})
})
this.connection.on('PlayerReconnected', (data) => {
this.onMessage({
messageId: Date.now().toString(),
type: 'PLAYER_RECONNECT',
timestamp: Date.now(),
gameId: this.gameId,
userId: data.userId,
data,
sequenceNumber: 0
})
})
// 消息确认
this.connection.on('MessageReceived', (ack) => {
this.handleMessageAck(ack.originalMessageId)
})
// 心跳响应
this.connection.on('HeartbeatResponse', (data) => {
// 计算网络延迟
const now = Date.now()
const latency = now - (data.clientTimestamp || now)
console.log(`Network latency: ${latency}ms`)
})
// 错误处理
this.connection.on('Error', (error) => {
console.error('Server error:', error)
this.onMessage({
messageId: Date.now().toString(),
type: 'ERROR',
timestamp: Date.now(),
gameId: this.gameId,
userId: '',
data: error,
sequenceNumber: 0
})
})
// 连接状态变化
this.connection.onreconnecting(() => {
this.onConnectionStateChange('RECONNECTING')
this.stopHeartbeat()
})
this.connection.onreconnected(() => {
this.onConnectionStateChange('CONNECTED')
this.startHeartbeat()
this.resendQueuedMessages()
this.requestGameState() // 重连后同步状态
})
this.connection.onclose(() => {
this.onConnectionStateChange('DISCONNECTED')
this.stopHeartbeat()
})
}
private handleReceivedMessage(message: WebSocketMessage): void {
// 检查消息序列号,处理消息丢失
if (message.sequenceNumber && message.sequenceNumber > this.lastReceivedSequence + 1) {
console.warn(`Message sequence gap detected: expected ${this.lastReceivedSequence + 1}, got ${message.sequenceNumber}`)
// 请求缺失的消息或完整状态同步
this.requestGameState()
}
this.lastReceivedSequence = Math.max(this.lastReceivedSequence, message.sequenceNumber || 0)
this.onMessage(message)
}
async sendMessage(type: MessageType, data: any, requiresAck: boolean = true): Promise<void> {
const message: WebSocketMessage = {
messageId: this.generateMessageId(),
type,
timestamp: Date.now(),
gameId: this.gameId,
userId: this.getCurrentUserId(),
data,
sequenceNumber: ++this.sequenceNumber,
requiresAck
}
if (this.connection?.state === 'Connected') {
try {
await this.sendDirectMessage(message)
// 如果需要确认,加入等待队列
if (requiresAck) {
this.messageQueue.push({
message,
attempts: 0,
timestamp: Date.now()
})
}
} catch (error) {
console.error('Send message failed:', error)
this.queueMessage(message)
}
} else {
this.queueMessage(message)
}
}
private async sendDirectMessage(message: WebSocketMessage): Promise<void> {
switch (message.type) {
case 'ATTACK_POSITION':
await this.connection!.invoke('AttackPosition', message)
break
case 'PLACE_PLANES':
await this.connection!.invoke('PlacePlanes', message)
break
case 'HEARTBEAT':
await this.connection!.invoke('Heartbeat', message)
break
default:
console.warn(`Unknown message type: ${message.type}`)
}
}
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(async () => {
try {
await this.sendMessage('HEARTBEAT', {
clientTimestamp: Date.now()
}, false)
} catch (error) {
console.error('Heartbeat failed:', error)
}
}, 10000) // 10秒心跳
}
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = null
}
}
private async requestGameState(): Promise<void> {
if (this.connection?.state === 'Connected') {
try {
await this.connection.invoke('RequestGameState', this.gameId)
} catch (error) {
console.error('Request game state failed:', error)
}
}
}
private queueMessage(message: WebSocketMessage): void {
this.messageQueue.push({
message,
attempts: 0,
timestamp: Date.now()
})
}
private async resendQueuedMessages(): Promise<void> {
const pendingMessages = [...this.messageQueue]
this.messageQueue = []
for (const queuedMessage of pendingMessages) {
if (queuedMessage.attempts < 3) { // 最多重试3次
try {
await this.sendDirectMessage(queuedMessage.message)
queuedMessage.attempts++
if (queuedMessage.message.requiresAck) {
this.messageQueue.push(queuedMessage)
}
} catch (error) {
console.error('Resend message failed:', error)
queuedMessage.attempts++
if (queuedMessage.attempts < 3) {
this.messageQueue.push(queuedMessage)
}
}
}
}
}
private handleMessageAck(messageId: string): void {
this.messageQueue = this.messageQueue.filter(
queuedMessage => queuedMessage.message.messageId !== messageId
)
}
private generateMessageId(): string {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
private getCurrentUserId(): string {
// 从认证token或存储中获取用户ID
return 'current-user-id' // 实现细节
}
async disconnect(): Promise<void> {
this.stopHeartbeat()
if (this.connection) {
await this.connection.stop()
}
this.onConnectionStateChange('DISCONNECTED')
}
}
interface QueuedMessage {
message: WebSocketMessage
attempts: number
timestamp: number
}
type ConnectionState = 'CONNECTING' | 'CONNECTED' | 'RECONNECTING' | 'DISCONNECTED'
```
## 5. 关键 .NET 服务实现
### 5.1 连接管理服务
```csharp
public interface IConnectionManager
{
Task RegisterConnection(string userId, string connectionId, string gameId);
Task UnregisterConnection(string connectionId);
Task UpdateLastActivity(string connectionId);
Task<List<string>> GetUserConnections(string userId);
Task<bool> IsUserOnline(string userId);
}
public class ConnectionManager : IConnectionManager
{
private readonly IMemoryCache _cache;
private readonly ILogger<ConnectionManager> _logger;
public ConnectionManager(IMemoryCache cache, ILogger<ConnectionManager> logger)
{
_cache = cache;
_logger = logger;
}
public async Task RegisterConnection(string userId, string connectionId, string gameId)
{
var connectionInfo = new ConnectionInfo
{
UserId = userId,
ConnectionId = connectionId,
GameId = gameId,
ConnectedAt = DateTime.UtcNow,
LastActivity = DateTime.UtcNow
};
_cache.Set($"connection_{connectionId}", connectionInfo, TimeSpan.FromHours(2));
// 更新用户连接列表
var userConnections = await GetUserConnections(userId);
userConnections.Add(connectionId);
_cache.Set($"user_connections_{userId}", userConnections, TimeSpan.FromHours(2));
_logger.LogInformation($"User {userId} connected with connection {connectionId}");
}
public async Task UnregisterConnection(string connectionId)
{
if (_cache.TryGetValue($"connection_{connectionId}", out ConnectionInfo connectionInfo))
{
_cache.Remove($"connection_{connectionId}");
var userConnections = await GetUserConnections(connectionInfo.UserId);
userConnections.Remove(connectionId);
_cache.Set($"user_connections_{connectionInfo.UserId}", userConnections);
_logger.LogInformation($"Connection {connectionId} unregistered");
}
}
public async Task UpdateLastActivity(string connectionId)
{
if (_cache.TryGetValue($"connection_{connectionId}", out ConnectionInfo connectionInfo))
{
connectionInfo.LastActivity = DateTime.UtcNow;
_cache.Set($"connection_{connectionId}", connectionInfo, TimeSpan.FromHours(2));
}
}
public async Task<List<string>> GetUserConnections(string userId)
{
if (_cache.TryGetValue($"user_connections_{userId}", out List<string> connections))
{
return connections;
}
return new List<string>();
}
public async Task<bool> IsUserOnline(string userId)
{
var connections = await GetUserConnections(userId);
return connections.Any();
}
}
public class ConnectionInfo
{
public string UserId { get; set; }
public string ConnectionId { get; set; }
public string GameId { get; set; }
public DateTime ConnectedAt { get; set; }
public DateTime LastActivity { get; set; }
}
```
### 5.2 消息确认和重发机制
```csharp
public class MessageReliabilityService
{
private readonly IMemoryCache _cache;
private readonly IHubContext<GameHub> _hubContext;
public async Task SendReliableMessage(string connectionId, string method, object data)
{
var messageId = Guid.NewGuid().ToString();
var message = new ReliableMessage
{
MessageId = messageId,
Method = method,
Data = data,
SentAt = DateTime.UtcNow,
Attempts = 0
};
// 存储消息等待确认
_cache.Set($"pending_message_{messageId}", message, TimeSpan.FromMinutes(5));
// 发送消息
await _hubContext.Clients.Client(connectionId).SendAsync(method, data);
// 设置超时重发
_ = Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(async _ =>
{
await CheckAndResendMessage(messageId, connectionId);
});
}
public async Task ConfirmMessage(string messageId)
{
_cache.Remove($"pending_message_{messageId}");
}
private async Task CheckAndResendMessage(string messageId, string connectionId)
{
if (_cache.TryGetValue($"pending_message_{messageId}", out ReliableMessage message))
{
message.Attempts++;
if (message.Attempts < 3) // 最多重试3次
{
await _hubContext.Clients.Client(connectionId).SendAsync(message.Method, message.Data);
// 更新缓存
_cache.Set($"pending_message_{messageId}", message, TimeSpan.FromMinutes(5));
// 再次设置超时
_ = Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(async _ =>
{
await CheckAndResendMessage(messageId, connectionId);
});
}
else
{
// 超过重试次数,移除消息
_cache.Remove($"pending_message_{messageId}");
}
}
}
}
public class ReliableMessage
{
public string MessageId { get; set; }
public string Method { get; set; }
public object Data { get; set; }
public DateTime SentAt { get; set; }
public int Attempts { get; set; }
}
```
## 6. Startup.cs 配置
```csharp
public void ConfigureServices(IServiceCollection services)
{
// SignalR配置
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
options.KeepAliveInterval = TimeSpan.FromSeconds(10);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
}).AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
// CORS配置
services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder
.WithOrigins("https://your-miniprogram-domain.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// 服务注册
services.AddScoped<IGameService, GameService>();
services.AddSingleton<IConnectionManager, ConnectionManager>();
services.AddSingleton<MessageReliabilityService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// SignalR Hub路由
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<GameHub>("/game-hub");
endpoints.MapControllers();
});
}
```
这个WebSocket接口规范提供了完整的断线重连策略包括指数退避重连、消息队列管理、心跳机制、状态同步等关键功能。.NET后端通过SignalR提供了可靠的WebSocket服务支持自动重连和消息确认机制。

View File

@@ -0,0 +1,119 @@
# “打飞机”小程序App原型设计需求文档
## 1. 概述
本文档旨在为“打飞机”小程序的核心界面提供清晰的设计需求主要面向UI/UX设计人员用于指导原型设计。文档充分考虑了手机触摸屏的操作特性。核心界面包括“准备页面”和“打击页面”。
---
## 2. 核心页面设计需求
### 2.1 准备页面
**页面目标:** 玩家在此页面中通过触控操作完成飞机的布局,并确认进入“准备就绪”状态。
#### 2.1.1 界面核心元素
1. **游戏棋盘 (10x10 网格):**
* **功能:** 用于放置飞机的主要区域。
* **设计要求:**
* 应为一个 10x10 的网格布局,每个格子大小一致,适合触控点击。
* 格子有清晰的边框。
* **触控反馈:** 点击格子时,应有视觉反馈(如背景色瞬时变化或涟漪效果)。
* 坐标标识在棋盘的上方和左侧应有坐标轴上方为字母A-J左侧为数字1-10
2. **飞机状态与选择区:**
* **功能:** 展示可用的飞机,并允许玩家选择要操作的飞机。
* **设计要求:**
* 提供三个按钮分别代表“飞机1”、“飞机2”、“飞机3”。
* 当一架飞机被成功放置到棋盘上后,对应的按钮应变为“选中”或“已放置”状态(如改变颜色或样式)。
* 当玩家在棋盘上**点击**已放置的飞机时,此区域对应的飞机按钮也应同步显示为“当前选中”状态。
* 如果所有飞机都已放置,此区域的按钮都应处于“已放置”状态。
3. **布局操作按钮组 (针对触控优化):**
* **功能:** 控制飞机的放置、移动和旋转。
* **设计要求:**
* **方向控制:** “上、下、左、右”四个方向按钮,用于在添加飞机前,预设飞机的机头朝向。**此为第一种放置方式的辅助按钮。**
* **移动控制:** 提供四个方向箭头按钮(上、下、左、右),用于在棋盘上移动当前选中的飞机。
* **旋转控制:** 提供一个旋转按钮,用于顺时针旋转当前选中的飞机。
* **删除按钮:** 用于删除当前在棋盘上选中的飞机。
4. **准备确认按钮组:**
* **功能:** 玩家完成布局后,进行最终确认。
* **设计要求:**
* **完成按钮:**
* 当棋盘上没有满3架飞机时此按钮应为“禁用”状态。
* 当3架飞机都已放置好后此按钮变为“可用”状态。
* **删除按钮:** (此处有重复,建议整合)一个“清空”或“重置”按钮,用于一键清除所有已放置的飞机,方便重新布局。
5. **状态提示区:**
* **功能:** 向玩家反馈当前的游戏状态。
* **设计要求:**
* 在页面底部或一个显著位置,显示文本信息,如“请放置三架飞机”、“已准备,等待对手中...”、“对手已准备就绪”。
#### 2.1.2 交互流程
1. **进入页面:** 默认显示一个空的10x10棋盘。
2. **放置飞机:**
* **直接点击机身**
* 玩家直接点击棋盘上的一个**空白**格子。
* 系统以此格为飞机的中心点(或机身的一部分),自动生成一架默认朝向(例如,机头朝上)的飞机。
* 如果空间不足以放置飞机应给出提示如格子闪烁红色或toast提示且不放置飞机。
* **通用规则:**
* 飞机成功放置后“飞机x”按钮状态改变表示一架飞机已被使用。
* 当3架飞机都放置完毕后放置功能自动禁用。
3. **选择与操作飞机:**
* **单击**棋盘上已放置的飞机,该飞机进入“选中”状态(如高亮或边框变化),此时可对它进行移动或旋转。
* 使用移动和旋转按钮调整“选中”飞机的位置和形态。
* 点击“删除”按钮,移除选中的飞机。
4. **完成准备:**
* 当3架飞机全部放置后“完成”按钮激活。
* 玩家点击“完成”,布局操作按钮全部变为“禁用”状态,页面提示“已准备,等待对手中...”。
---
### 2.2 打击页面
**页面目标:** 玩家在此页面通过触控选择坐标进行攻击,并查看攻击结果。
#### 2.2.1 界面核心元素
1. **攻击棋盘 (10x10 网格):**
* **功能:** 展示对手的区域,供玩家选择打击坐标。
* **设计要求:**
* 与准备页面的棋盘样式基本一致,带坐标轴,格子大小易于触控。
* **格子状态:** 需要清晰地区分以下几种状态:
* **未打击:** 默认状态。
* **待确认打击:** 玩家**点击**格子后,在按下“打击”按钮前的状态(如背景变为“巧克力色”)。棋盘上同时只能有一个“待确认”的格子。
* **未命中:** 打击后服务器返回结果为“未命中”的状态如显示一个“X”或特定图标
* **命中:** 打击后,服务器返回结果为“命中”的状态(如格子变色,显示火焰等)。
* **击毁:** 打击后,服务器返回结果为“击毁”的状态(如显示更强烈的击毁效果,并将该飞机的其余所有“命中”格子一同标记为“已击毁”)。
2. **操作与状态区:**
* **功能:** 执行打击操作和显示信息。
* **设计要求:**
* **打击按钮:**
* 当玩家在棋盘上选择了一个“待确认打击”的格子后,此按钮变为“可用”状态。
* 点击后,按钮进入“加载中/打击中...”状态,直到服务器返回结果。
* **状态提示:** 显示当前回合信息,如“轮到你攻击”、“等待对手攻击...”。
* **攻击结果反馈:** 每次攻击后,应有弹窗或醒目提示,明确告知玩家“命中”、“未命中”或“击毁”。
3. **我方飞机布局参考图 (可选但建议):**
* **功能:** 在打击页面的一个角落或侧边,以缩略图形式展示玩家自己在准备阶段的飞机布局。
* **设计要求:**
* 这是一个只读视图,不可交互。
* 当自己的飞机被对手击中时,此参考图上的对应位置也应同步更新状态。
#### 2.2.2 交互流程
1. **进入页面:** 轮到玩家攻击时进入此页面。显示一个干净的10x10攻击棋盘。
2. **选择目标:** 玩家在棋盘上**点击**一个目标格子,该格子变为“待确认打击”状态,“打击”按钮激活。如果玩家点击其他格子,则新的格子变为“待确认”,旧的格子恢复默认。
3. **确认打击:** 玩家点击“打击”按钮。
4. **等待结果:** 按钮禁用,棋盘暂时不可点击,等待服务器返回结果。
5. **展示结果:**
* 服务器返回结果后,棋盘上对应的格子根据结果(命中/未命中/击毁)更新其视觉状态。
* 弹出提示,告知玩家打击结果。
6. **回合结束:** 页面自动跳转到“等待页面”,或直接在当前页面显示“等待对手攻击...”的遮罩,直到下一回合开始。

View File

@@ -0,0 +1,183 @@
# 打飞机小程序 - 简化主页面线框图设计
## 设计说明
根据用户需求,主页面简化为只包含以下元素:
- 顶部:用户头像和昵称
- 中间:游戏标题
- 规则说明
- 开始按钮
## 线框图设计方案
### 方案一:居中简约布局
```
┌─────────────────────────────────────────────────────────┐
│ │
│ [头像] [昵称] │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 打飞机对战 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 游戏规则 │ │
│ │ 1. 双人轮流攻击对方棋盘 │ │
│ │ 2. 击中对方飞机部件得分 │ │
│ │ 3. 先击毁对方所有飞机获胜 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 开始游戏 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### 方案二:卡片式布局
```
┌─────────────────────────────────────────────────────────┐
│ │
│ [头像] [昵称] │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 打飞机对战小程序 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 📖 游戏规则 │ │
│ │ │ │
│ │ 双人轮流攻击对方棋盘 │ │
│ │ 击中飞机部件得分 │ │
│ │ 击毁所有飞机获胜 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🚀 开始游戏 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### 方案三:渐变背景布局
```
┌─────────────────────────────────────────────────────────┐
│ │
│ [头像] [昵称] │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ✈️ 打飞机对战小程序 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🎮 游戏规则 │ │
│ │ │ │
│ │ • 双人轮流攻击 │ │
│ │ • 击中飞机部件得分 │ │
│ │ • 击毁所有飞机获胜 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🎯 开始游戏 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### 方案四:图标装饰布局
```
┌─────────────────────────────────────────────────────────┐
│ │
│ [头像] [昵称] │
│ │
│ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 打飞机对战小程序 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 📋 游戏规则 │ │
│ │ 1. 双人轮流攻击棋盘 │ │
│ │ 2. 击中飞机部件得分 │ │
│ │ 3. 击毁所有飞机获胜 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ ✈️ 🛩️ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🚀 开始游戏 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### 方案五:分步骤布局
```
┌─────────────────────────────────────────────────────────┐
│ │
│ [头像] [昵称] │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 打飞机对战小程序 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 📖 游戏规则 │ │
│ │ │ │
│ │ 步骤1: 双人轮流攻击对方棋盘 │ │
│ │ 步骤2: 击中飞机部件获得分数 │ │
│ │ 步骤3: 先击毁对方所有飞机获胜 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🎮 开始游戏 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
## 设计建议
根据打飞机游戏的特性和目标用户群体,我推荐**方案二:卡片式布局**,原因如下:
1. **视觉层次清晰**:卡片设计让内容分区明确,用户一目了然
2. **现代感强**:卡片式设计符合当前移动应用设计趋势
3. **易于交互**:卡片按钮在移动端触控体验良好
4. **扩展性好**:后续如需添加内容,卡片布局便于调整
5. **适合游戏场景**:卡片式设计给人一种"游戏卡"的感觉,符合游戏主题
请确认您偏好的布局方案,我将进入下一阶段:**主题设计**,包括色彩方案、字体选择、间距系统等视觉元素的设计。

View File

@@ -0,0 +1,606 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>打飞机对战小程序</title>
<meta name="theme-color" content="#0f1419">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<style>
/* 设计变量 */
:root {
/* 主色调 - 深空科技蓝 */
--primary-color: #6366f1;
--primary-light: #8b5cf6;
--primary-dark: #4f46e5;
/* 辅助色 - 科技青色 */
--secondary-color: #40e0d0;
--secondary-light: #26d0ce;
/* 强调色 - 橙色 */
--accent-color: #f59e0b;
--accent-light: #fbbf24;
/* 危险色 - 红色 */
--danger-color: #ff4757;
--danger-light: #ff6b6b;
/* 背景色系 - 深色渐变 */
--bg-primary: #0f1419;
--bg-secondary: #1a1d29;
--bg-tertiary: #252837;
--bg-elevated: #2d3142;
/* 渐变背景 */
--gradient-bg: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 30%, #16213e 70%, #0f3460 100%);
/* 边框色系 */
--border-primary: #3d4159;
--border-secondary: #4a5073;
--border-highlight: #6366f1;
--border-accent: rgba(64, 224, 208, 0.3);
/* 文字色系 */
--text-primary: #ffffff;
--text-secondary: #b4b7c9;
--text-tertiary: #9ca3af;
--text-disabled: #6b7280;
/* 移动端触摸区域尺寸 */
--touch-target-min: 44px;
--safe-area-padding: 20px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
html, body {
height: 100%;
overflow-x: hidden;
-webkit-text-size-adjust: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Microsoft YaHei UI", sans-serif;
background: var(--gradient-bg);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
touch-action: manipulation;
}
/* 安全区域适配 */
.safe-area {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
/* 动态星空背景 */
.star-field {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
}
.star {
position: absolute;
background: rgba(64, 224, 208, 0.8);
border-radius: 50%;
animation: twinkle 3s infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
}
/* 主容器 */
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
z-index: 1;
}
/* 顶部用户信息栏 */
.user-header {
background: rgba(26, 29, 41, 0.95);
backdrop-filter: blur(20px);
padding: 15px var(--safe-area-padding);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-primary);
position: sticky;
top: 0;
z-index: 100;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.user-details h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.user-level {
font-size: 12px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 4px;
}
.level-badge {
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-light) 100%);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
}
.settings-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(99, 102, 241, 0.15);
border: 1px solid var(--primary-color);
color: var(--primary-color);
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.settings-btn:hover {
background: rgba(99, 102, 241, 0.25);
transform: scale(1.05);
}
.settings-btn:active {
transform: scale(0.95);
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: var(--safe-area-padding);
justify-content: center;
align-items: center;
text-align: center;
position: relative;
}
/* 游戏标题卡片 */
.title-card {
width: 100%;
max-width: 400px;
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 16px;
padding: 30px 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
margin-bottom: 30px;
position: relative;
overflow: hidden;
}
.title-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color));
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { transform: translateX(-100%); }
50% { transform: translateX(100%); }
}
.game-title {
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 50%, var(--secondary-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
position: relative;
}
.game-subtitle {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 20px;
}
/* 飞机装饰 */
.plane-decoration {
position: absolute;
opacity: 0.1;
font-size: 60px;
color: var(--primary-color);
}
.plane-decoration.top-left {
top: 10px;
left: 10px;
transform: rotate(-45deg);
}
.plane-decoration.top-right {
top: 10px;
right: 10px;
transform: rotate(45deg);
}
.plane-decoration.bottom-left {
bottom: 10px;
left: 10px;
transform: rotate(135deg);
}
.plane-decoration.bottom-right {
bottom: 10px;
right: 10px;
transform: rotate(-135deg);
}
/* 游戏规则卡片 */
.rules-card {
width: 100%;
max-width: 400px;
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 16px;
padding: 25px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
margin-bottom: 30px;
}
.rules-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.rules-icon {
width: 24px;
height: 24px;
background: linear-gradient(135deg, var(--secondary-color) 0%, var(--secondary-light) 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #1a1a2e;
font-weight: 600;
}
.rules-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.rules-content {
text-align: left;
}
.rule-item {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.5;
}
.rule-item:last-child {
margin-bottom: 0;
}
.rule-number {
min-width: 20px;
height: 20px;
background: rgba(99, 102, 241, 0.2);
border: 1px solid var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
color: var(--primary-color);
margin-top: 2px;
}
/* 开始按钮 */
.start-button {
width: 100%;
max-width: 300px;
min-height: var(--touch-target-min);
border: none;
border-radius: 12px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
padding: 16px 32px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
color: #ffffff;
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
user-select: none;
-webkit-tap-highlight-color: transparent;
position: relative;
overflow: hidden;
}
.start-button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
}
.start-button:hover {
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(99, 102, 241, 0.5);
}
.start-button:active {
transform: translateY(0);
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
}
.start-button:active::before {
width: 100%;
height: 100%;
}
.button-icon {
font-size: 20px;
}
/* 响应式设计 */
@media (max-width: 375px) {
.user-header {
padding: 12px var(--safe-area-padding);
}
.user-avatar {
width: 36px;
height: 36px;
font-size: 16px;
}
.game-title {
font-size: 24px;
}
.start-button {
font-size: 16px;
min-height: 42px;
padding: 14px 24px;
}
}
@media (max-height: 640px) {
.main-content {
padding-top: 15px;
padding-bottom: 15px;
}
.title-card, .rules-card {
margin-bottom: 20px;
padding: 20px;
}
.game-title {
margin-bottom: 8px;
}
}
/* 高对比度支持 */
@media (prefers-contrast: high) {
:root {
--text-primary: #ffffff;
--text-secondary: #e5e5e5;
--border-primary: #ffffff;
--bg-secondary: #000000;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 暗色模式强制支持 */
@media (prefers-color-scheme: dark) {
body {
background: var(--bg-primary);
color: var(--text-primary);
}
}
</style>
</head>
<body class="safe-area">
<!-- 动态星空背景 -->
<div class="star-field" id="starField"></div>
<div class="app-container">
<!-- 顶部用户信息栏 -->
<header class="user-header">
<div class="user-info">
<div class="user-avatar">👤</div>
<div class="user-details">
<h3>玩家昵称</h3>
</div>
</div>
<button class="settings-btn">⚙️</button>
</header>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 游戏标题卡片 -->
<div class="title-card">
<div class="plane-decoration top-left">✈️</div>
<div class="plane-decoration top-right">✈️</div>
<div class="plane-decoration bottom-left">✈️</div>
<div class="plane-decoration bottom-right">✈️</div>
<h1 class="game-title">打飞机对战</h1>
<p class="game-subtitle">经典策略游戏,智慧与技巧的较量</p>
</div>
<!-- 游戏规则卡片 -->
<div class="rules-card">
<div class="rules-header">
<div class="rules-icon">📖</div>
<h2 class="rules-title">游戏规则</h2>
</div>
<div class="rules-content">
<div class="rule-item">
<div class="rule-number">1</div>
<div>双人轮流攻击对方棋盘,猜测飞机位置</div>
</div>
<div class="rule-item">
<div class="rule-number">2</div>
<div>击中飞机部件获得分数,未命中则轮到对手</div>
</div>
<div class="rule-item">
<div class="rule-number">3</div>
<div>先击毁对方所有飞机的玩家获胜</div>
</div>
</div>
</div>
<!-- 开始按钮 -->
<button class="start-button" id="startGame">
<span class="button-icon">🚀</span>
开始游戏
</button>
</main>
</div>
<script>
// 创建动态星空背景
function createStarField() {
const starField = document.getElementById('starField');
const numStars = 50;
for (let i = 0; i < numStars; i++) {
const star = document.createElement('div');
star.className = 'star';
star.style.width = Math.random() * 3 + 'px';
star.style.height = star.style.width;
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.animationDelay = Math.random() * 3 + 's';
starField.appendChild(star);
}
}
// 触觉反馈
function addHapticFeedback(element) {
element.addEventListener('click', () => {
if ('vibrate' in navigator) {
navigator.vibrate(10);
}
document.body.classList.add('haptic-feedback');
setTimeout(() => document.body.classList.remove('haptic-feedback'), 100);
});
}
// 开始游戏按钮点击事件
document.getElementById('startGame').addEventListener('click', () => {
// 这里可以添加跳转到游戏页面的逻辑
console.log('开始游戏');
if ('vibrate' in navigator) {
navigator.vibrate(20);
}
});
// 设置按钮点击事件
document.querySelector('.settings-btn').addEventListener('click', () => {
console.log('打开设置');
if ('vibrate' in navigator) {
navigator.vibrate(10);
}
});
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
createStarField();
// 为所有可点击元素添加触觉反馈
addHapticFeedback(document.getElementById('startGame'));
addHapticFeedback(document.querySelector('.settings-btn'));
});
</script>
</body>
</html>

View File

@@ -0,0 +1,630 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>飞机布置 - 新版</title>
<meta name="theme-color" content="#0f1419">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<style>
:root {
--primary-color: #6366f1;
--primary-light: #8b5cf6;
--accent-color: #f59e0b;
--danger-color: #ef4444;
--success-color: #10b981;
--bg-primary: #0f1419;
--bg-secondary: #1a1d29;
--bg-tertiary: #252837;
--border-primary: #3d4159;
--border-secondary: #4a5073;
--border-highlight: #6366f1;
--text-primary: #ffffff;
--text-secondary: #b4b7c9;
--text-tertiary: #9ca3af;
--text-disabled: #6b7280;
--cell-size: min(8.5vw, 38px);
--safe-area-padding: 16px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; -webkit-user-select: none; user-select: none; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Microsoft YaHei UI", sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
touch-action: none;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
.app-container { display: flex; flex-direction: column; height: 100%; }
.top-nav {
padding: 12px var(--safe-area-padding);
border-bottom: 1px solid var(--border-primary);
text-align: center;
font-weight: 600;
background: rgba(15, 20, 25, 0.8);
backdrop-filter: blur(10px);
}
.main-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: var(--safe-area-padding);
}
.board-wrapper { position: relative; }
.game-board {
display: grid;
grid-template-columns: repeat(10, var(--cell-size));
grid-template-rows: repeat(10, var(--cell-size));
gap: 1px;
background: var(--border-primary);
border: 1px solid var(--border-primary);
}
.board-cell {
background: var(--bg-secondary);
transition: background-color 0.2s;
}
.board-cell:active { background-color: var(--border-highlight); }
.board-cell.plane-part { background-color: var(--primary-color); }
.board-cell.plane-head { background-color: var(--accent-color); }
.board-cell.plane-body { background-color: var(--primary-light); }
.board-cell.plane-wing { background-color: var(--primary-light); }
.board-cell.plane-tail { background-color: var(--primary-color); }
.board-cell.selected { box-shadow: inset 0 0 0 2px var(--success-color); z-index: 1; }
.col-label, .row-label {
position: absolute;
font-size: 10px;
color: var(--text-tertiary);
display: flex;
align-items: center;
justify-content: center;
}
.col-label { top: -20px; height: 15px; width: var(--cell-size); }
.row-label { left: -20px; width: 15px; height: var(--cell-size); }
.controls-area {
display: flex;
flex-direction: column;
gap: 12px;
padding: var(--safe-area-padding);
background: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
}
.main-controls {
display: flex;
gap: 16px;
justify-content: space-between;
align-items: flex-start;
}
.direction-control {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.direction-grid {
display: grid;
grid-template-columns: repeat(3, 44px);
grid-template-rows: repeat(3, 44px);
gap: 4px;
}
.direction-grid button {
width: 44px;
height: 44px;
}
.btn {
padding: 10px;
border-radius: 8px;
border: 1px solid var(--border-primary);
background: var(--bg-tertiary);
color: var(--text-primary);
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
min-width: 44px;
min-height: 44px;
}
.plane-selection {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
margin-right: 20px;
}
.confirmation-group {
display: flex;
justify-content: space-around;
gap: 10px;
}
.plane-btn.selected {
background-color: var(--success-color) !important;
border-color: var(--success-color) !important;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
transform: scale(1.05);
}
.status-display {
flex-grow: 1;
text-align: center;
font-size: 14px;
color: var(--text-secondary);
align-self: center;
}
.btn {
padding: 10px;
border-radius: 8px;
border: 1px solid var(--border-primary);
background: var(--bg-tertiary);
color: var(--text-primary);
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
min-width: 44px;
min-height: 44px;
}
.btn:disabled {
background: var(--bg-tertiary);
color: var(--text-disabled);
border-color: var(--border-primary);
opacity: 0.6;
cursor: not-allowed;
}
.btn:active:not(:disabled) { transform: scale(0.95); }
.plane-btn.placed { background-color: var(--primary-color); border-color: var(--primary-light); }
.plane-btn.selected { background-color: var(--success-color); border-color: var(--success-color); }
.confirmation-group .btn-primary { background-color: var(--primary-color); }
.confirmation-group .btn-danger { background-color: var(--danger-color); }
</style>
</head>
<body>
<div class="app-container">
<header class="top-nav">准备页面</header>
<main class="main-content">
<div class="board-wrapper" id="boardWrapper">
<div class="game-board" id="gameBoard"></div>
</div>
</main>
<footer class="controls-area">
<div class="status-display" id="statusDisplay">请点击棋盘放置飞机</div>
<div class="main-controls">
<div class="direction-control">
<div class="direction-grid">
<div></div>
<button class="btn" id="moveUpBtn"></button>
<button class="btn" id="deleteBtn">🗑️</button>
<button class="btn" id="moveLeftBtn"></button>
<button class="btn" id="rotateBtn"></button>
<button class="btn" id="moveRightBtn"></button>
<div></div>
<button class="btn" id="moveDownBtn"></button>
<div></div>
</div>
</div>
<div class="plane-selection">
<button class="btn plane-btn" id="planeBtn1" data-plane-id="1">飞机1</button>
<button class="btn plane-btn" id="planeBtn2" data-plane-id="2">飞机2</button>
<button class="btn plane-btn" id="planeBtn3" data-plane-id="3">飞机3</button>
</div>
</div>
<div class="confirmation-group">
<button class="btn" id="randomBtn">🎲 随机</button>
<button class="btn btn-danger" id="resetBtn">🔄 重置</button>
<button class="btn btn-primary" id="doneBtn">✓ 完成</button>
</div>
</footer>
</div>
<script>
class MobilePlacementGame {
constructor() {
this.BOARD_SIZE = 10;
this.PLANE_COUNT = 3;
this.boardState = Array(this.BOARD_SIZE).fill(null).map(() => Array(this.BOARD_SIZE).fill(0));
this.planes = [];
this.selectedPlaneId = null;
this.dom = {
board: document.getElementById('gameBoard'),
boardWrapper: document.getElementById('boardWrapper'),
statusDisplay: document.getElementById('statusDisplay'),
planeBtns: [
document.getElementById('planeBtn1'),
document.getElementById('planeBtn2'),
document.getElementById('planeBtn3'),
],
moveUpBtn: document.getElementById('moveUpBtn'),
moveDownBtn: document.getElementById('moveDownBtn'),
moveLeftBtn: document.getElementById('moveLeftBtn'),
moveRightBtn: document.getElementById('moveRightBtn'),
rotateBtn: document.getElementById('rotateBtn'),
deleteBtn: document.getElementById('deleteBtn'),
randomBtn: document.getElementById('randomBtn'),
resetBtn: document.getElementById('resetBtn'),
doneBtn: document.getElementById('doneBtn'),
};
this.init();
}
init() {
this.createBoard();
this.createPlanes();
this.bindEvents();
this.updateUI();
console.log("游戏已初始化");
}
createBoard() {
this.dom.board.innerHTML = '';
for (let r = 0; r < this.BOARD_SIZE; r++) {
for (let c = 0; c < this.BOARD_SIZE; c++) {
const cell = document.createElement('div');
cell.className = 'board-cell';
cell.dataset.row = r;
cell.dataset.col = c;
this.dom.board.appendChild(cell);
}
}
const labelsWrapper = document.createElement('div');
labelsWrapper.className = 'labels';
for (let i = 0; i < this.BOARD_SIZE; i++) {
const colLabel = document.createElement('div');
colLabel.className = 'col-label';
colLabel.textContent = String.fromCharCode(65 + i);
colLabel.style.left = `calc(${(i + 0.5)} * var(--cell-size))`;
labelsWrapper.appendChild(colLabel);
const rowLabel = document.createElement('div');
rowLabel.className = 'row-label';
rowLabel.textContent = i + 1;
rowLabel.style.top = `calc(${(i + 0.5)} * var(--cell-size))`;
labelsWrapper.appendChild(rowLabel);
}
this.dom.boardWrapper.appendChild(labelsWrapper);
}
createPlanes() {
this.planes = [];
for(let i = 1; i <= this.PLANE_COUNT; i++) {
this.planes.push({
id: i,
center: null,
direction: 'up',
isPlaced: false,
});
}
}
bindEvents() {
this.dom.board.addEventListener('click', this.handleBoardClick.bind(this));
this.dom.planeBtns.forEach(btn => btn.addEventListener('click', this.handlePlaneBtnClick.bind(this)));
this.dom.moveUpBtn.addEventListener('click', () => this.moveSelectedPlane(0, -1));
this.dom.moveDownBtn.addEventListener('click', () => this.moveSelectedPlane(0, 1));
this.dom.moveLeftBtn.addEventListener('click', () => this.moveSelectedPlane(-1, 0));
this.dom.moveRightBtn.addEventListener('click', () => this.moveSelectedPlane(1, 0));
this.dom.rotateBtn.addEventListener('click', this.rotateSelectedPlane.bind(this));
this.dom.deleteBtn.addEventListener('click', this.deleteSelectedPlane.bind(this));
this.dom.randomBtn.addEventListener('click', this.randomPlacePlane.bind(this));
this.dom.resetBtn.addEventListener('click', this.resetGame.bind(this));
this.dom.doneBtn.addEventListener('click', this.completePlacement.bind(this));
}
handleBoardClick(e) {
const cell = e.target.closest('.board-cell');
if (!cell) return;
const row = parseInt(cell.dataset.row);
const col = parseInt(cell.dataset.col);
const cellContent = this.boardState[row][col];
if (cellContent > 0) { // Clicked on an existing plane
this.selectedPlaneId = cellContent;
} else { // Clicked on an empty cell
const unplacedPlane = this.planes.find(p => !p.isPlaced);
if (unplacedPlane) {
this.tryPlacePlane(unplacedPlane.id, {x: col, y: row}, 'up');
} else {
this.updateStatus("所有飞机都已放置。");
}
}
this.updateUI();
}
handlePlaneBtnClick(e) {
const planeId = parseInt(e.target.dataset.planeId);
const plane = this.planes.find(p => p.id === planeId);
if (plane && plane.isPlaced) {
this.selectedPlaneId = (this.selectedPlaneId === planeId) ? null : planeId;
this.updateUI();
} else {
this.updateStatus(`飞机 ${planeId} 尚未放置。`);
}
}
tryPlacePlane(planeId, center, direction) {
const plane = this.planes.find(p => p.id === planeId);
if (!plane) return;
const directions = ['up', 'right', 'down', 'left'];
const startIndex = directions.indexOf(direction);
let placedSuccessfully = false;
let usedDirection = direction;
// 尝试所有方向,从指定方向开始
for (let i = 0; i < directions.length; i++) {
const currentDirection = directions[(startIndex + i) % 4];
const positions = this.getPlanePositions(center, currentDirection);
if (this.isPlacementValid(positions, planeId)) {
// Clear old position if any
this.clearPlaneFromBoard(planeId);
plane.center = center;
plane.direction = currentDirection;
plane.isPlaced = true;
positions.forEach(p => { this.boardState[p.y][p.x] = planeId; });
this.selectedPlaneId = planeId;
placedSuccessfully = true;
usedDirection = currentDirection;
// 如果使用了不同于初始请求的方向,显示特殊提示
if (currentDirection !== direction) {
this.updateStatus(`飞机 ${planeId} 已放置,已自动旋转为 ${this.getDirectionName(currentDirection)} 方向。`);
} else {
this.updateStatus(`飞机 ${planeId} 操作成功。`);
}
break;
}
}
if (!placedSuccessfully) {
this.updateStatus(`操作无效: 所有方向都存在位置冲突或越界。`);
}
this.updateUI();
}
moveSelectedPlane(dx, dy) {
if (!this.selectedPlaneId) return;
const plane = this.planes.find(p => p.id === this.selectedPlaneId);
const newCenter = { x: plane.center.x + dx, y: plane.center.y + dy };
this.tryPlacePlane(plane.id, newCenter, plane.direction);
}
rotateSelectedPlane() {
if (!this.selectedPlaneId) return;
const plane = this.planes.find(p => p.id === this.selectedPlaneId);
const directions = ['up', 'right', 'down', 'left'];
const currentIndex = directions.indexOf(plane.direction);
const nextDirection = directions[(currentIndex + 1) % 4];
this.tryPlacePlane(plane.id, plane.center, nextDirection);
}
deleteSelectedPlane() {
if (!this.selectedPlaneId) return;
const planeId = this.selectedPlaneId;
this.clearPlaneFromBoard(planeId);
const plane = this.planes.find(p => p.id === planeId);
plane.isPlaced = false;
plane.center = null;
this.selectedPlaneId = null;
this.updateStatus(`飞机 ${planeId} 已移除。`);
this.updateUI();
}
randomPlacePlane() {
const unplacedPlane = this.planes.find(p => !p.isPlaced);
if (!unplacedPlane) {
this.updateStatus("所有飞机都已放置。");
return;
}
// 尝试随机位置和方向
const directions = ['up', 'right', 'down', 'left'];
const maxAttempts = 100; // 防止无限循环
let placed = false;
for (let attempt = 0; attempt < maxAttempts && !placed; attempt++) {
const randomX = Math.floor(Math.random() * this.BOARD_SIZE);
const randomY = Math.floor(Math.random() * this.BOARD_SIZE);
const randomDirection = directions[Math.floor(Math.random() * directions.length)];
const center = { x: randomX, y: randomY };
const positions = this.getPlanePositions(center, randomDirection);
if (this.isPlacementValid(positions, unplacedPlane.id)) {
this.clearPlaneFromBoard(unplacedPlane.id);
unplacedPlane.center = center;
unplacedPlane.direction = randomDirection;
unplacedPlane.isPlaced = true;
positions.forEach(p => { this.boardState[p.y][p.x] = unplacedPlane.id; });
this.selectedPlaneId = unplacedPlane.id;
placed = true;
this.updateStatus(`飞机 ${unplacedPlane.id} 已随机放置在 ${String.fromCharCode(65 + randomX)}${randomY + 1},方向为${this.getDirectionName(randomDirection)}`);
}
}
if (!placed) {
this.updateStatus(`无法为飞机 ${unplacedPlane.id} 找到合适的随机位置。`);
}
this.updateUI();
}
resetGame() {
this.boardState = Array(this.BOARD_SIZE).fill(null).map(() => Array(this.BOARD_SIZE).fill(0));
this.createPlanes();
this.selectedPlaneId = null;
this.updateStatus("已重置棋盘,请重新放置。");
this.updateUI();
}
completePlacement() {
if(this.planes.every(p => p.isPlaced)) {
this.updateStatus("准备就绪,等待对手...");
localStorage.setItem('playerPlanes', JSON.stringify(this.planes));
// 禁用所有按钮
Object.values(this.dom).flat().forEach(el => {
if(el.tagName === 'BUTTON') el.disabled = true;
});
} else {
this.updateStatus("请先放置所有飞机。");
}
}
clearPlaneFromBoard(planeId) {
for (let r = 0; r < this.BOARD_SIZE; r++) {
for (let c = 0; c < this.BOARD_SIZE; c++) {
if (this.boardState[r][c] === planeId) {
this.boardState[r][c] = 0;
}
}
}
}
getPlanePositions(center, direction) {
const geometry = {
up: [
{ x: 0, y: -2, type: 'head' },
{ x: -2, y: -1, type: 'wing' }, { x: -1, y: -1, type: 'wing' }, { x: 0, y: -1, type: 'wing' }, { x: 1, y: -1, type: 'wing' }, { x: 2, y: -1, type: 'wing' },
{ x: 0, y: 0, type: 'body' }, { x: 0, y: 1, type: 'body' },
{ x: -1, y: 2, type: 'tail' }, { x: 0, y: 2, type: 'tail' }, { x: 1, y: 2, type: 'tail' }
],
down: [
{ x: 0, y: 2, type: 'head' },
{ x: -2, y: 1, type: 'wing' }, { x: -1, y: 1, type: 'wing' }, { x: 0, y: 1, type: 'wing' }, { x: 1, y: 1, type: 'wing' }, { x: 2, y: 1, type: 'wing' },
{ x: 0, y: 0, type: 'body' }, { x: 0, y: -1, type: 'body' },
{ x: -1, y: -2, type: 'tail' }, { x: 0, y: -2, type: 'tail' }, { x: 1, y: -2, type: 'tail' }
],
left: [
{ x: -2, y: 0, type: 'head' },
{ x: -1, y: -2, type: 'wing' }, { x: -1, y: -1, type: 'wing' }, { x: -1, y: 0, type: 'wing' }, { x: -1, y: 1, type: 'wing' }, { x: -1, y: 2, type: 'wing' },
{ x: 0, y: 0, type: 'body' }, { x: 1, y: 0, type: 'body' },
{ x: 2, y: -1, type: 'tail' }, { x: 2, y: 0, type: 'tail' }, { x: 2, y: 1, type: 'tail' }
],
right: [
{ x: 2, y: 0, type: 'head' },
{ x: 1, y: -2, type: 'wing' }, { x: 1, y: -1, type: 'wing' }, { x: 1, y: 0, type: 'wing' }, { x: 1, y: 1, type: 'wing' }, { x: 1, y: 2, type: 'wing' },
{ x: 0, y: 0, type: 'body' }, { x: -1, y: 0, type: 'body' },
{ x: -2, y: -1, type: 'tail' }, { x: -2, y: 0, type: 'tail' }, { x: -2, y: 1, type: 'tail' }
]
};
const offsets = geometry[direction];
if (!offsets) return [];
return offsets.map(o => ({ x: center.x + o.x, y: center.y + o.y, type: o.type }));
}
isPlacementValid(positions, planeId) {
for (const pos of positions) {
if (pos.x < 0 || pos.x >= this.BOARD_SIZE || pos.y < 0 || pos.y >= this.BOARD_SIZE) return false; // Out of bounds
const occupyingId = this.boardState[pos.y][pos.x];
if (occupyingId !== 0 && occupyingId !== planeId) return false; // Collision
}
return true;
}
getDirectionName(direction) {
const directionNames = {
'up': '向上',
'right': '向右',
'down': '向下',
'left': '向左'
};
return directionNames[direction] || direction;
}
updateUI() {
// Update board
for (let r = 0; r < this.BOARD_SIZE; r++) {
for (let c = 0; c < this.BOARD_SIZE; c++) {
const cell = this.dom.board.children[r * this.BOARD_SIZE + c];
cell.className = 'board-cell';
const planeId = this.boardState[r][c];
if (planeId > 0) {
cell.classList.add('plane-part');
const plane = this.planes.find(p => p.id === planeId);
if (plane && plane.isPlaced && plane.center) {
const planePositions = this.getPlanePositions(plane.center, plane.direction);
const part = planePositions.find(p => p.x === c && p.y === r);
if (part && part.type) {
cell.classList.add(`plane-${part.type}`); // Adds plane-head, plane-body etc.
}
if (planeId === this.selectedPlaneId) {
cell.classList.add('selected');
}
}
}
}
}
// Update plane buttons
this.dom.planeBtns.forEach(btn => {
const planeId = parseInt(btn.dataset.planeId);
const plane = this.planes.find(p => p.id === planeId);
btn.classList.remove('placed', 'selected');
if (plane.isPlaced) btn.classList.add('placed');
if (planeId === this.selectedPlaneId) btn.classList.add('selected');
});
// Update manipulation buttons
const isPlaneSelected = this.selectedPlaneId !== null;
const hasUnplacedPlane = this.planes.some(p => !p.isPlaced);
this.dom.moveUpBtn.disabled = !isPlaneSelected;
this.dom.moveDownBtn.disabled = !isPlaneSelected;
this.dom.moveLeftBtn.disabled = !isPlaneSelected;
this.dom.moveRightBtn.disabled = !isPlaneSelected;
this.dom.rotateBtn.disabled = !isPlaneSelected;
this.dom.deleteBtn.disabled = !isPlaneSelected;
this.dom.randomBtn.disabled = !hasUnplacedPlane;
// Update confirmation buttons
this.dom.doneBtn.disabled = !this.planes.every(p => p.isPlaced);
}
updateStatus(message) {
this.dom.statusDisplay.textContent = message;
}
}
document.addEventListener('DOMContentLoaded', () => new MobilePlacementGame());
</script>
</body>
</html>

View File

@@ -0,0 +1,677 @@
/* 移动端控件样式设计规范 - 示例CSS */
:root {
/* 主色调 - 深空科技蓝 */
--primary-color: #6366f1;
--primary-light: #8b5cf6;
--primary-dark: #4f46e5;
/* 辅助色 - 科技青色 */
--secondary-color: #40e0d0;
--secondary-light: #26d0ce;
/* 强调色 - 橙色 */
--accent-color: #f59e0b;
--accent-light: #fbbf24;
/* 危险色 - 红色 */
--danger-color: #ff4757;
--danger-light: #ff6b6b;
/* 背景色系 - 深色渐变 */
--bg-primary: #0f1419;
--bg-secondary: #1a1d29;
--bg-tertiary: #252837;
--bg-elevated: #2d3142;
/* 渐变背景 */
--gradient-bg: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 30%, #16213e 70%, #0f3460 100%);
/* 边框色系 */
--border-primary: #3d4159;
--border-secondary: #4a5073;
--border-highlight: #6366f1;
--border-accent: rgba(64, 224, 208, 0.3);
/* 文字色系 */
--text-primary: #ffffff;
--text-secondary: #b4b7c9;
--text-tertiary: #9ca3af;
--text-disabled: #6b7280;
/* 移动端触摸区域尺寸 */
--touch-target-min: 44px;
--safe-area-padding: 20px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
html, body {
height: 100%;
overflow-x: hidden;
-webkit-text-size-adjust: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Microsoft YaHei UI", sans-serif;
background: var(--gradient-bg);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
touch-action: manipulation;
}
/* PWA 支持的安全区域适配 */
.safe-area {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
/* 动态星空背景 */
.star-field {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
}
.star {
position: absolute;
background: rgba(64, 224, 208, 0.8);
border-radius: 50%;
animation: twinkle 3s infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
}
/* 主容器 - 全屏布局 */
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
z-index: 1;
}
/* 状态栏 */
.status-bar {
background: rgba(26, 29, 41, 0.95);
backdrop-filter: blur(20px);
padding: 8px var(--safe-area-padding);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-tertiary);
position: sticky;
top: 0;
z-index: 100;
}
.network-status {
display: flex;
align-items: center;
gap: 6px;
}
.connection-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--secondary-color);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(1.3); }
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: var(--safe-area-padding);
justify-content: center;
align-items: center;
text-align: center;
position: relative;
}
.page-title {
font-size: 24px;
font-weight: 700;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 20px;
}
/* 页签切换容器 */
.tab-container {
width: 100%;
max-width: 400px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 16px;
overflow: hidden;
margin-bottom: 20px;
}
.tab-header {
display: flex;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-primary);
}
.tab-item {
flex: 1;
padding: 12px;
text-align: center;
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.tab-item.active {
color: var(--primary-color);
background: var(--bg-secondary);
border-bottom: 2px solid var(--primary-color);
}
.tab-content {
padding: 20px;
max-height: 400px;
overflow-y: auto;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 按钮控件 */
.btn {
min-height: var(--touch-target-min);
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
padding: 14px 24px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
user-select: none;
-webkit-tap-highlight-color: transparent;
position: relative;
overflow: hidden;
}
/* 按钮触摸反馈效果 */
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
}
.btn:active::before {
width: 100%;
height: 100%;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
color: #ffffff;
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover, .btn-primary:focus {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.4);
}
.btn-primary:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
}
.btn-success {
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
color: #1a1a2e;
box-shadow: 0 4px 16px rgba(64, 224, 208, 0.3);
}
.btn-success:hover, .btn-success:focus {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(64, 224, 208, 0.4);
}
.btn-secondary {
background: rgba(99, 102, 241, 0.15);
color: var(--primary-color);
border: 1px solid var(--primary-color);
backdrop-filter: blur(10px);
}
.btn-secondary:hover, .btn-secondary:focus {
background: rgba(99, 102, 241, 0.25);
transform: translateY(-1px);
}
/* 按钮组 */
.action-buttons {
width: 100%;
max-width: 300px;
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 20px;
}
/* 卡片控件 */
.card {
width: 100%;
max-width: 400px;
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 16px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-primary);
}
.card-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.card-subtitle {
font-size: 14px;
color: var(--text-secondary);
}
.card-body {
color: var(--text-secondary);
font-size: 14px;
line-height: 1.6;
text-align: left;
}
.card-footer {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border-primary);
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 统计卡片 */
.stats-card {
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 12px;
padding: 16px;
text-align: center;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
}
.stats-title {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 8px;
}
.stats-value {
font-size: 20px;
font-weight: bold;
color: var(--secondary-color);
text-shadow: 0 0 10px rgba(64, 224, 208, 0.3);
}
/* 列表控件 */
.list-container {
width: 100%;
max-width: 400px;
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 16px;
overflow: hidden;
backdrop-filter: blur(10px);
margin-bottom: 20px;
}
.list-item {
padding: 16px;
border-bottom: 1px solid rgba(64, 224, 208, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.list-item:last-child {
border-bottom: none;
}
.list-item:active {
background: rgba(64, 224, 208, 0.1);
transform: scale(0.98);
}
.list-item-title {
font-size: 16px;
color: var(--text-primary);
font-weight: 500;
text-align: left;
}
.list-item-subtitle {
font-size: 14px;
color: var(--text-secondary);
margin-top: 4px;
text-align: left;
}
.list-item-value {
font-size: 16px;
color: var(--secondary-color);
font-weight: 600;
}
/* 历史记录列表 */
.history-list {
max-height: 200px;
overflow-y: auto;
background: rgba(15, 52, 96, 0.4);
border-radius: 12px;
padding: 12px;
width: 100%;
max-width: 400px;
}
.history-item {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 8px;
padding: 8px;
background: rgba(64, 224, 208, 0.1);
border-radius: 8px;
border-left: 3px solid var(--secondary-color);
transition: all 0.3s ease;
text-align: left;
}
.history-item:last-child {
margin-bottom: 0;
}
.history-item.hit {
border-left-color: var(--danger-color);
background: rgba(255, 71, 87, 0.1);
}
.history-item.miss {
border-left-color: var(--text-tertiary);
background: rgba(108, 117, 125, 0.1);
}
.history-item.destroy {
border-left-color: var(--accent-color);
background: rgba(245, 158, 11, 0.2);
font-weight: 600;
}
/* 网格控件 */
.game-grid {
width: 100%;
aspect-ratio: 1;
background: rgba(15, 52, 96, 0.6);
border: 2px solid rgba(64, 224, 208, 0.5);
border-radius: 12px;
padding: 6px;
backdrop-filter: blur(8px);
position: relative;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 400px;
}
.grid-container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(10, 1fr);
gap: 1px;
background: rgba(64, 224, 208, 0.1);
border-radius: 8px;
overflow: hidden;
}
.grid-cell {
background: rgba(15, 52, 96, 0.8);
border: 1px solid rgba(64, 224, 208, 0.15);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: rgba(255, 255, 255, 0.4);
transition: all 0.2s ease;
position: relative;
min-height: 30px;
font-weight: 600;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.grid-cell:active {
background: rgba(64, 224, 208, 0.4);
transform: scale(0.95);
border-color: var(--secondary-color);
}
.grid-cell.hit {
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-light) 100%);
color: white;
animation: hitFlash 0.6s ease-out;
box-shadow: 0 0 15px var(--danger-color);
}
.grid-cell.miss {
background: rgba(108, 117, 125, 0.6);
color: var(--text-tertiary);
}
@keyframes hitFlash {
0% {
transform: scale(1);
box-shadow: 0 0 10px var(--danger-color);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 20px var(--danger-color);
}
100% {
transform: scale(1);
box-shadow: 0 0 5px var(--danger-color);
}
}
/* 状态指示器 */
.network-status-indicator {
position: fixed;
top: calc(15px + env(safe-area-inset-top));
right: 15px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--secondary-color);
z-index: 101;
opacity: 0.8;
}
.network-status-indicator.offline {
background: var(--danger-color);
animation: networkPulse 2s infinite;
}
@keyframes networkPulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 0.3; }
}
.turn-indicator {
position: fixed;
top: calc(70px + env(safe-area-inset-top));
left: 50%;
transform: translateX(-50%);
padding: 8px 20px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
z-index: 99;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.turn-indicator.my-turn {
background: linear-gradient(135deg, var(--secondary-color) 0%, var(--secondary-light) 100%);
color: #1a1a2e;
box-shadow: 0 4px 20px rgba(64, 224, 208, 0.4);
}
.turn-indicator.opponent-turn {
background: linear-gradient(135deg, rgba(255, 71, 87, 0.9) 0%, rgba(255, 56, 56, 0.9) 100%);
color: white;
border: 1px solid rgba(255, 71, 87, 0.6);
box-shadow: 0 4px 20px rgba(255, 71, 87, 0.3);
}
/* 触摸反馈 */
.haptic-feedback {
animation: hapticVibrate 0.1s ease-out;
}
@keyframes hapticVibrate {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
/* 响应式设计增强 */
@media (max-width: 375px) {
.btn {
font-size: 15px;
min-height: 42px;
padding: 12px 20px;
}
.grid-cell {
font-size: 8px;
min-height: 28px;
}
.stats-value {
font-size: 16px;
}
}
@media (max-height: 640px) {
.main-content {
justify-content: flex-start;
padding-top: 20px;
}
.page-title {
margin-bottom: 10px;
}
.card {
margin: 10px 0;
padding: 16px;
}
}
/* 高对比度支持 */
@media (prefers-contrast: high) {
:root {
--text-primary: #ffffff;
--text-secondary: #e5e5e5;
--border-primary: #ffffff;
--bg-secondary: #000000;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 暗色模式强制支持 */
@media (prefers-color-scheme: dark) {
body {
background: var(--bg-primary);
color: var(--text-primary);
}
}

View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>移动端控件样式示例</title>
<meta name="theme-color" content="#0f1419">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="stylesheet" href="移动端控件样式示例.css">
</head>
<body class="safe-area">
<div class="app-container">
<div class="status-bar">
<div class="network-status">
<div class="connection-dot"></div>
<span>在线</span>
</div>
<div class="app-version">v1.0.0</div>
</div>
<main class="main-content">
<h1 class="page-title">移动端控件样式示例</h1>
<div class="tab-container">
<div class="tab-header">
<div class="tab-item active" data-tab="tab1">卡片控件</div>
<div class="tab-item" data-tab="tab2">列表控件</div>
<div class="tab-item" data-tab="tab3">网格控件</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="tab1">
<div class="card">
<div class="card-header">
<h3 class="card-title">基础卡片</h3>
<span class="card-subtitle">副标题</span>
</div>
<div class="card-body">
<p>这是卡片内容区域,可以放置各种信息。卡片控件是移动端应用中最常用的容器之一。</p>
</div>
<div class="card-footer">
<button class="btn btn-secondary">取消</button>
<button class="btn btn-primary">确定</button>
</div>
</div>
<div class="stats-card">
<div class="stats-title">统计项</div>
<div class="stats-value">100</div>
</div>
</div>
<div class="tab-pane" id="tab2">
<div class="list-container">
<div class="list-item">
<div>
<div class="list-item-title">列表项1</div>
<div class="list-item-subtitle">描述信息</div>
</div>
<div class="list-item-value">值1</div>
</div>
<div class="list-item">
<div>
<div class="list-item-title">列表项2</div>
<div class="list-item-subtitle">描述信息</div>
</div>
<div class="list-item-value">值2</div>
</div>
<div class="list-item">
<div>
<div class="list-item-title">列表项3</div>
<div class="list-item-subtitle">描述信息</div>
</div>
<div class="list-item-value">值3</div>
</div>
</div>
<div class="history-list">
<div class="history-item">玩家攻击 A3: 命中机翼</div>
<div class="history-item hit">敌方攻击 B5: 命中机身</div>
<div class="history-item miss">玩家攻击 C7: 未命中</div>
<div class="history-item destroy">敌方攻击 D2: 击毁敌机!</div>
</div>
</div>
<div class="tab-pane" id="tab3">
<div class="game-grid">
<div class="grid-container">
<!-- 生成10x10网格 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const gridContainer = document.querySelector('#tab3 .grid-container');
for (let i = 0; i < 100; i++) {
const cell = document.createElement('div');
cell.className = 'grid-cell';
cell.textContent = String.fromCharCode(65 + Math.floor(i / 10)) + (i % 10 + 1);
// 添加一些示例状态
if (i === 22) cell.classList.add('hit');
if (i === 45) cell.classList.add('miss');
if (i === 67) cell.classList.add('hit');
gridContainer.appendChild(cell);
}
});
</script>
</div>
</div>
</div>
</div>
</div>
<div class="action-buttons">
<button class="btn btn-primary">主要操作</button>
<button class="btn btn-success">成功操作</button>
<button class="btn btn-secondary">次要操作</button>
</div>
</main>
</div>
<!-- 动态星空背景 -->
<div class="star-field" id="starField"></div>
<!-- 网络状态指示器 -->
<div class="network-status-indicator"></div>
<!-- 回合指示器 -->
<div class="turn-indicator my-turn">我的回合</div>
<script>
// 页签切换功能
document.querySelectorAll('.tab-item').forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
// 更新标签状态
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// 更新内容显示
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
});
});
// 触觉反馈
document.querySelectorAll('.btn, .list-item, .tab-item').forEach(element => {
element.addEventListener('click', () => {
if ('vibrate' in navigator) {
navigator.vibrate(10);
}
document.body.classList.add('haptic-feedback');
setTimeout(() => document.body.classList.remove('haptic-feedback'), 100);
});
});
// 创建动态星空背景
function createStarField() {
const starField = document.getElementById('starField');
const numStars = 50;
for (let i = 0; i < numStars; i++) {
const star = document.createElement('div');
star.className = 'star';
star.style.width = Math.random() * 3 + 'px';
star.style.height = star.style.width;
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.animationDelay = Math.random() * 3 + 's';
starField.appendChild(star);
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
createStarField();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,963 @@
# 移动端控件样式设计规范
## 1. 设计理念
### 1.1 核心原则
- **移动优先**:专为手机端设计,确保在小屏幕上的最佳体验
- **避免滚动**:页面内容尽量在一屏内展示,过长内容使用页签切换
- **深色主题**:采用深色背景,减少视觉疲劳,增强科技感
- **高对比度**:确保文字和控件在各种环境下清晰可见
### 1.2 视觉风格
- **科技感**:使用渐变色彩和毛玻璃效果,营造现代科技氛围
- **简洁明了**:控件设计简洁,功能明确,避免复杂装饰
- **一致性**:所有页面和控件保持统一的设计语言
## 2. 色彩系统
### 2.1 主色调
```css
:root {
/* 主色调 - 深空科技蓝 */
--primary-color: #6366f1;
--primary-light: #8b5cf6;
--primary-dark: #4f46e5;
/* 辅助色 - 科技青色 */
--secondary-color: #40e0d0;
--secondary-light: #26d0ce;
/* 强调色 - 橙色 */
--accent-color: #f59e0b;
--accent-light: #fbbf24;
/* 危险色 - 红色 */
--danger-color: #ff4757;
--danger-light: #ff6b6b;
}
```
### 2.2 背景色系
```css
:root {
/* 背景色系 - 深色渐变 */
--bg-primary: #0f1419;
--bg-secondary: #1a1d29;
--bg-tertiary: #252837;
--bg-elevated: #2d3142;
/* 渐变背景 */
--gradient-bg: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 30%, #16213e 70%, #0f3460 100%);
}
```
### 2.3 边框色系
```css
:root {
/* 边框色系 */
--border-primary: #3d4159;
--border-secondary: #4a5073;
--border-highlight: #6366f1;
--border-accent: rgba(64, 224, 208, 0.3);
}
```
### 2.4 文字色系
```css
:root {
/* 文字色系 */
--text-primary: #ffffff;
--text-secondary: #b4b7c9;
--text-tertiary: #9ca3af;
--text-disabled: #6b7280;
}
```
## 3. 布局原则
### 3.1 安全区域适配
```css
.safe-area {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
```
### 3.2 触摸目标尺寸
```css
:root {
--touch-target-min: 44px;
--safe-area-padding: 20px;
}
```
### 3.3 页签切换设计
当内容过长时,使用页签切换替代滚动:
```html
<div class="tab-container">
<div class="tab-header">
<div class="tab-item active" data-tab="tab1">标签1</div>
<div class="tab-item" data-tab="tab2">标签2</div>
<div class="tab-item" data-tab="tab3">标签3</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="tab1">内容1</div>
<div class="tab-pane" id="tab2">内容2</div>
<div class="tab-pane" id="tab3">内容3</div>
</div>
</div>
<style>
.tab-container {
width: 100%;
max-width: 400px;
background: var(--bg-secondary);
border-radius: 16px;
overflow: hidden;
}
.tab-header {
display: flex;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-primary);
}
.tab-item {
flex: 1;
padding: 12px;
text-align: center;
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s ease;
}
.tab-item.active {
color: var(--primary-color);
background: var(--bg-secondary);
border-bottom: 2px solid var(--primary-color);
}
.tab-content {
padding: 20px;
max-height: 400px;
overflow-y: auto;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
```
## 4. 控件样式规范
### 4.1 按钮控件
#### 4.1.1 主要按钮
```css
.btn-primary {
min-height: var(--touch-target-min);
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
padding: 14px 24px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
color: #ffffff;
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.btn-primary:hover, .btn-primary:focus {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.4);
}
.btn-primary:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
}
```
#### 4.1.2 成功按钮
```css
.btn-success {
min-height: var(--touch-target-min);
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
padding: 14px 24px;
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
color: #1a1a2e;
box-shadow: 0 4px 16px rgba(64, 224, 208, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.btn-success:hover, .btn-success:focus {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(64, 224, 208, 0.4);
}
```
#### 4.1.3 次要按钮
```css
.btn-secondary {
min-height: var(--touch-target-min);
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
padding: 14px 24px;
background: rgba(99, 102, 241, 0.15);
color: var(--primary-color);
border: 1px solid var(--primary-color);
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.btn-secondary:hover, .btn-secondary:focus {
background: rgba(99, 102, 241, 0.25);
transform: translateY(-1px);
}
```
### 4.2 卡片控件
#### 4.2.1 基础卡片
```css
.card {
width: 100%;
max-width: 400px;
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 16px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-primary);
}
.card-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.card-subtitle {
font-size: 14px;
color: var(--text-secondary);
}
.card-body {
color: var(--text-secondary);
font-size: 14px;
line-height: 1.6;
}
.card-footer {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border-primary);
display: flex;
justify-content: flex-end;
gap: 12px;
}
```
#### 4.2.2 统计卡片
```css
.stats-card {
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 12px;
padding: 16px;
text-align: center;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.stats-title {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 8px;
}
.stats-value {
font-size: 20px;
font-weight: bold;
color: var(--secondary-color);
text-shadow: 0 0 10px rgba(64, 224, 208, 0.3);
}
```
### 4.3 列表控件
#### 4.3.1 基础列表
```css
.list-container {
width: 100%;
max-width: 400px;
background: rgba(22, 33, 62, 0.8);
border: 1px solid rgba(64, 224, 208, 0.3);
border-radius: 16px;
overflow: hidden;
backdrop-filter: blur(10px);
}
.list-item {
padding: 16px;
border-bottom: 1px solid rgba(64, 224, 208, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
}
.list-item:last-child {
border-bottom: none;
}
.list-item:active {
background: rgba(64, 224, 208, 0.1);
transform: scale(0.98);
}
.list-item-title {
font-size: 16px;
color: var(--text-primary);
font-weight: 500;
}
.list-item-subtitle {
font-size: 14px;
color: var(--text-secondary);
margin-top: 4px;
}
.list-item-value {
font-size: 16px;
color: var(--secondary-color);
font-weight: 600;
}
```
#### 4.3.2 历史记录列表
```css
.history-list {
max-height: 200px;
overflow-y: auto;
background: rgba(15, 52, 96, 0.4);
border-radius: 12px;
padding: 12px;
}
.history-item {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 8px;
padding: 8px;
background: rgba(64, 224, 208, 0.1);
border-radius: 8px;
border-left: 3px solid var(--secondary-color);
transition: all 0.3s ease;
}
.history-item:last-child {
margin-bottom: 0;
}
.history-item.hit {
border-left-color: var(--danger-color);
background: rgba(255, 71, 87, 0.1);
}
.history-item.miss {
border-left-color: var(--text-tertiary);
background: rgba(108, 117, 125, 0.1);
}
.history-item.destroy {
border-left-color: var(--accent-color);
background: rgba(245, 158, 11, 0.2);
font-weight: 600;
}
```
### 4.4 网格控件
#### 4.4.1 游戏网格
```css
.game-grid {
width: 100%;
aspect-ratio: 1;
background: rgba(15, 52, 96, 0.6);
border: 2px solid rgba(64, 224, 208, 0.5);
border-radius: 12px;
padding: 6px;
backdrop-filter: blur(8px);
position: relative;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.grid-container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(10, 1fr);
gap: 1px;
background: rgba(64, 224, 208, 0.1);
border-radius: 8px;
overflow: hidden;
}
.grid-cell {
background: rgba(15, 52, 96, 0.8);
border: 1px solid rgba(64, 224, 208, 0.15);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: rgba(255, 255, 255, 0.4);
transition: all 0.2s ease;
position: relative;
min-height: 30px;
font-weight: 600;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.grid-cell:active {
background: rgba(64, 224, 208, 0.4);
transform: scale(0.95);
border-color: var(--secondary-color);
}
.grid-cell.hit {
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-light) 100%);
color: white;
animation: hitFlash 0.6s ease-out;
box-shadow: 0 0 15px var(--danger-color);
}
.grid-cell.miss {
background: rgba(108, 117, 125, 0.6);
color: var(--text-tertiary);
}
@keyframes hitFlash {
0% {
transform: scale(1);
box-shadow: 0 0 10px var(--danger-color);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 20px var(--danger-color);
}
100% {
transform: scale(1);
box-shadow: 0 0 5px var(--danger-color);
}
}
```
### 4.5 状态指示器
#### 4.5.1 网络状态
```css
.network-status {
position: fixed;
top: calc(15px + env(safe-area-inset-top));
right: 15px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--secondary-color);
z-index: 101;
opacity: 0.8;
}
.network-status.offline {
background: var(--danger-color);
animation: networkPulse 2s infinite;
}
@keyframes networkPulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 0.3; }
}
```
#### 4.5.2 回合指示器
```css
.turn-indicator {
position: fixed;
top: calc(70px + env(safe-area-inset-top));
left: 50%;
transform: translateX(-50%);
padding: 8px 20px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
z-index: 99;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.turn-indicator.my-turn {
background: linear-gradient(135deg, var(--secondary-color) 0%, var(--secondary-light) 100%);
color: #1a1a2e;
box-shadow: 0 4px 20px rgba(64, 224, 208, 0.4);
}
.turn-indicator.opponent-turn {
background: linear-gradient(135deg, rgba(255, 71, 87, 0.9) 0%, rgba(255, 56, 56, 0.9) 100%);
color: white;
border: 1px solid rgba(255, 71, 87, 0.6);
box-shadow: 0 4px 20px rgba(255, 71, 87, 0.3);
}
```
## 5. 动画效果
### 5.1 背景动画
```css
/* 动态星空背景 */
.star-field {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
}
.star {
position: absolute;
background: rgba(64, 224, 208, 0.8);
border-radius: 50%;
animation: twinkle 3s infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
}
```
### 5.2 按钮触摸反馈
```css
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
}
.btn:active::before {
width: 100%;
height: 100%;
}
```
### 5.3 页面切换动画
```css
.page-transition-enter {
opacity: 0;
transform: translateX(100%);
}
.page-transition-enter-active {
opacity: 1;
transform: translateX(0);
transition: all 0.3s ease-out;
}
.page-transition-exit {
opacity: 1;
transform: translateX(0);
}
.page-transition-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 0.3s ease-in;
}
```
## 6. 移动端适配
### 6.1 响应式断点
```css
/* 小屏幕手机 */
@media (max-width: 375px) {
.btn {
font-size: 15px;
min-height: 42px;
padding: 12px 20px;
}
.grid-cell {
font-size: 8px;
min-height: 28px;
}
.stats-value {
font-size: 16px;
}
}
/* 短屏幕手机 */
@media (max-height: 640px) {
.main-content {
justify-content: flex-start;
padding-top: 20px;
}
.game-header {
margin-bottom: 20px;
}
.card {
margin: 15px 0;
padding: 16px;
}
}
/* 横屏模式 */
@media (orientation: landscape) and (max-height: 500px) {
.game-container {
top: calc(70px + env(safe-area-inset-top));
}
.turn-indicator {
top: calc(40px + env(safe-area-inset-top));
}
.boards-container {
flex-direction: row;
max-width: 90%;
gap: 15px;
}
.board-section {
flex: 1;
}
}
```
### 6.2 触摸优化
```css
/* 防止双指缩放和滚动 */
body {
touch-action: manipulation;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
/* 触摸反馈 */
.haptic-feedback {
animation: hapticVibrate 0.1s ease-out;
}
@keyframes hapticVibrate {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
/* 防止长按菜单 */
* {
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
```
### 6.3 无障碍支持
```css
/* 高对比度支持 */
@media (prefers-contrast: high) {
:root {
--text-primary: #ffffff;
--text-secondary: #e5e5e5;
--border-primary: #ffffff;
--bg-secondary: #000000;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 暗色模式强制支持 */
@media (prefers-color-scheme: dark) {
body {
background: var(--bg-primary);
color: var(--text-primary);
}
}
```
## 7. 实现示例
### 7.1 完整页面示例
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>移动端页面示例</title>
<style>
/* 引入上述所有CSS变量和基础样式 */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Microsoft YaHei UI", sans-serif;
background: var(--gradient-bg);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.app-container {
flex: 1;
display: flex;
flex-direction: column;
padding: var(--safe-area-padding);
max-width: 100%;
margin: 0 auto;
}
.status-bar {
background: rgba(26, 29, 41, 0.95);
backdrop-filter: blur(20px);
padding: 8px 0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-tertiary);
margin-bottom: 20px;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
}
.page-title {
font-size: 24px;
font-weight: 700;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
}
.tab-container {
width: 100%;
max-width: 400px;
}
.action-buttons {
width: 100%;
max-width: 300px;
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 20px;
}
</style>
</head>
<body class="safe-area">
<div class="app-container">
<div class="status-bar">
<div class="network-status">
<div class="connection-dot"></div>
<span>在线</span>
</div>
<div class="app-version">v1.0.0</div>
</div>
<main class="main-content">
<h1 class="page-title">页面标题</h1>
<div class="tab-container">
<div class="tab-header">
<div class="tab-item active" data-tab="tab1">标签1</div>
<div class="tab-item" data-tab="tab2">标签2</div>
<div class="tab-item" data-tab="tab3">标签3</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="tab1">
<div class="card">
<div class="card-header">
<h3 class="card-title">卡片标题</h3>
<span class="card-subtitle">副标题</span>
</div>
<div class="card-body">
<p>这是卡片内容区域,可以放置各种信息。</p>
</div>
<div class="card-footer">
<button class="btn btn-secondary">取消</button>
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
<div class="tab-pane" id="tab2">
<div class="stats-card">
<div class="stats-title">统计项</div>
<div class="stats-value">100</div>
</div>
</div>
<div class="tab-pane" id="tab3">
<div class="list-container">
<div class="list-item">
<div>
<div class="list-item-title">列表项1</div>
<div class="list-item-subtitle">描述信息</div>
</div>
<div class="list-item-value">值1</div>
</div>
<div class="list-item">
<div>
<div class="list-item-title">列表项2</div>
<div class="list-item-subtitle">描述信息</div>
</div>
<div class="list-item-value">值2</div>
</div>
</div>
</div>
</div>
</div>
<div class="action-buttons">
<button class="btn btn-primary">主要操作</button>
<button class="btn btn-secondary">次要操作</button>
</div>
</main>
</div>
<script>
// 页签切换功能
document.querySelectorAll('.tab-item').forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
// 更新标签状态
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// 更新内容显示
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
});
});
// 触觉反馈
document.querySelectorAll('.btn, .list-item, .tab-item').forEach(element => {
element.addEventListener('click', () => {
if ('vibrate' in navigator) {
navigator.vibrate(10);
}
document.body.classList.add('haptic-feedback');
setTimeout(() => document.body.classList.remove('haptic-feedback'), 100);
});
});
</script>
</body>
</html>
```
## 8. 微信小程序适配建议
### 8.1 样式转换
- 将CSS变量转换为微信小程序的theme.json或全局样式
- 使用rpx单位替代px确保在不同屏幕尺寸下的适配
- 将backdrop-filter等不支持的效果替换为半透明背景
### 8.2 组件转换
- 将HTML控件转换为微信小程序的自定义组件
- 使用微信小程序的动画API替代CSS动画
- 利用微信小程序的页面生命周期管理状态
### 8.3 性能优化
- 使用微信小程序的虚拟列表处理长列表
- 优化图片资源,使用微信小程序的图片压缩
- 合理使用setData避免频繁更新页面数据
## 9. 总结
本设计规范基于提供的两个HTML文件的设计风格总结了一套适用于手机端微信小程序的控件样式设计规范。主要特点包括
1. **深色科技主题**:使用深色背景和渐变色彩,营造现代科技感
2. **移动优先**:专为手机端设计,考虑触摸操作和屏幕尺寸限制
3. **避免滚动**:通过页签切换等方式减少页面滚动
4. **统一视觉语言**:所有控件保持一致的设计风格
5. **丰富的交互反馈**:提供视觉、触觉等多种反馈方式
6. **良好的无障碍支持**:考虑高对比度、减少动画等特殊需求
通过遵循本设计规范,可以创建出用户体验良好、视觉一致的移动端应用界面。

File diff suppressed because it is too large Load Diff

1582
01_文档/游戏玩法.md Normal file

File diff suppressed because it is too large Load Diff