feat: 出牌记录序号修正、关于页面感谢名单、积分明细查看、JSON导入导出备份

This commit is contained in:
flykhan 2026-04-26 17:37:57 +08:00
parent 548fbef889
commit 2db44625bc
3 changed files with 203 additions and 7 deletions

View File

@ -38,7 +38,8 @@ data class ScoreEntry(
val duration: Int, val duration: Int,
val playerCount: Int, val playerCount: Int,
val turnNumber: Int, val turnNumber: Int,
val date: Long val date: Long,
val scoreDetail: String = ""
) )
object Scoreboard { object Scoreboard {
@ -66,7 +67,8 @@ object Scoreboard {
difficulty: String, difficulty: String,
duration: Int, duration: Int,
playerCount: Int, playerCount: Int,
turnNumber: Int turnNumber: Int,
scoreDetail: String = ""
) { ) {
val scores = loadScores(context).toMutableList() val scores = loadScores(context).toMutableList()
scores.add( scores.add(
@ -78,7 +80,8 @@ object Scoreboard {
duration = duration, duration = duration,
playerCount = playerCount, playerCount = playerCount,
turnNumber = turnNumber, turnNumber = turnNumber,
date = System.currentTimeMillis() date = System.currentTimeMillis(),
scoreDetail = scoreDetail
) )
) )
saveScores(context, scores.sortedByDescending { it.points }.take(50)) saveScores(context, scores.sortedByDescending { it.points }.take(50))
@ -158,6 +161,15 @@ fun LocalGameScreen(
active.score active.score
} }
} }
val scoreDetails = state.players.filter { it.id != myPlayerId }.flatMap { player ->
player.cards.map { card ->
val active = if (state.flipped && card.flipSide != null) card.flipSide else card
val label = if (active.type == CardType.NUMBER) "${active.color.displayName}${active.number}"
else "${active.color.displayName}${active.type.symbol}"
"${label}(${active.score}分)"
}
}
val detailStr = scoreDetails.joinToString(", ")
Scoreboard.addEntry( Scoreboard.addEntry(
context = context, context = context,
name = humanPlayerName, name = humanPlayerName,
@ -166,7 +178,8 @@ fun LocalGameScreen(
difficulty = gameDifficulty, difficulty = gameDifficulty,
duration = gameDuration, duration = gameDuration,
playerCount = totalPlayers, playerCount = totalPlayers,
turnNumber = gameTurnNumber turnNumber = gameTurnNumber,
scoreDetail = detailStr
) )
} }
} }
@ -315,7 +328,7 @@ fun LocalGameScreen(
gameLog.forEachIndexed { index, msg -> gameLog.forEachIndexed { index, msg ->
Row(modifier = Modifier.padding(vertical = 2.dp)) { Row(modifier = Modifier.padding(vertical = 2.dp)) {
Text( Text(
"${index + 1}. ", "${gameLog.size - index}. ",
color = Color.White.copy(alpha = 0.3f), color = Color.White.copy(alpha = 0.3f),
fontSize = 12.sp fontSize = 12.sp
) )

View File

@ -38,6 +38,7 @@ fun MainMenuScreen(
onNameChanged: (String) -> Unit onNameChanged: (String) -> Unit
) { ) {
var playerName by remember { mutableStateOf(initialName) } var playerName by remember { mutableStateOf(initialName) }
var showAbout by remember { mutableStateOf(false) }
Box( Box(
modifier = Modifier modifier = Modifier
@ -225,6 +226,15 @@ fun MainMenuScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// About button
TextButton(onClick = { showAbout = true }) {
Icon(Icons.Default.Info, null, tint = Color.White.copy(alpha = 0.5f))
Spacer(modifier = Modifier.width(6.dp))
Text("关于", color = Color.White.copy(alpha = 0.5f), fontSize = 14.sp)
}
Spacer(modifier = Modifier.height(8.dp))
// Instructions // Instructions
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -249,4 +259,29 @@ fun MainMenuScreen(
} }
} }
} }
// About dialog
if (showAbout) {
AlertDialog(
onDismissRequest = { showAbout = false },
title = { Text("关于", color = GoldAccent, fontWeight = FontWeight.Bold) },
text = {
Column {
Text("感谢名单", color = GoldAccent, fontSize = 16.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(12.dp))
Text("主创:刘博", color = Color.White, fontSize = 14.sp)
Spacer(modifier = Modifier.height(4.dp))
Text("最佳苦力opencode、deepseek v4 pro", color = Color.White, fontSize = 14.sp)
Spacer(modifier = Modifier.height(4.dp))
Text("金牌测试:张天弈、蔺明智", color = Color.White, fontSize = 14.sp)
}
},
confirmButton = {
TextButton(onClick = { showAbout = false }) {
Text("关闭", color = GoldAccent)
}
},
containerColor = DarkSurface
)
}
} }

View File

@ -1,12 +1,18 @@
package com.unogame.ui.screens package com.unogame.ui.screens
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
@ -19,6 +25,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.gson.Gson
import com.unogame.game.GameMode import com.unogame.game.GameMode
import com.unogame.ui.theme.* import com.unogame.ui.theme.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -28,10 +35,16 @@ import java.util.*
@Composable @Composable
fun ScoreboardScreen(onBack: () -> Unit) { fun ScoreboardScreen(onBack: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val scores = remember { Scoreboard.loadScores(context) } var scores by remember { mutableStateOf(Scoreboard.loadScores(context)) }
var selectedFilter by remember { mutableStateOf("全部") } var selectedFilter by remember { mutableStateOf("全部") }
val modes = listOf("全部") + GameMode.values().map { it.displayName } val modes = listOf("全部") + GameMode.values().map { it.displayName }
var showDetailDialog by remember { mutableStateOf(false) }
var detailEntry by remember { mutableStateOf<ScoreEntry?>(null) }
var showImportDialog by remember { mutableStateOf(false) }
var importJson by remember { mutableStateOf("") }
val gson = remember { Gson() }
val filtered = if (selectedFilter == "全部") scores val filtered = if (selectedFilter == "全部") scores
else scores.filter { it.mode == selectedFilter } else scores.filter { it.mode == selectedFilter }
@ -49,6 +62,38 @@ fun ScoreboardScreen(onBack: () -> Unit) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// Import / Export buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedButton(
onClick = {
val json = gson.toJson(scores)
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("uno_scoreboard", json))
Toast.makeText(context, "已复制备份JSON到剪贴板", Toast.LENGTH_SHORT).show()
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White.copy(alpha = 0.7f))
) {
Icon(Icons.Default.FileUpload, null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("导出备份", fontSize = 12.sp)
}
OutlinedButton(
onClick = { showImportDialog = true },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White.copy(alpha = 0.7f))
) {
Icon(Icons.Default.FileDownload, null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("导入备份", fontSize = 12.sp)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Mode filter chips // Mode filter chips
if (scores.isNotEmpty()) { if (scores.isNotEmpty()) {
Row( Row(
@ -106,7 +151,12 @@ fun ScoreboardScreen(onBack: () -> Unit) {
else "${entry.duration}" else "${entry.duration}"
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.clickable {
detailEntry = entry
showDetailDialog = true
},
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = if (index < 3) DarkSurface else DarkCard) colors = CardDefaults.cardColors(containerColor = if (index < 3) DarkSurface else DarkCard)
) { ) {
@ -179,4 +229,102 @@ fun ScoreboardScreen(onBack: () -> Unit) {
} }
} }
} }
// Detail dialog
if (showDetailDialog && detailEntry != null) {
val entry = detailEntry!!
val grouped = entry.scoreDetail.split(", ").filter { it.isNotEmpty() }
.groupBy { it }
.mapValues { it.value.size }
AlertDialog(
onDismissRequest = { showDetailDialog = false },
title = {
Text("${entry.name}${entry.mode}", color = GoldAccent, fontWeight = FontWeight.Bold, fontSize = 18.sp)
},
text = {
Column(
modifier = Modifier.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState())
) {
Text("总分: ${entry.points}", color = GoldAccent, fontSize = 16.sp, fontWeight = FontWeight.Black)
Spacer(modifier = Modifier.height(4.dp))
Text("难度: ${entry.difficulty} | ${entry.playerCount}人 | ${entry.turnNumber}",
color = Color.White.copy(alpha = 0.5f), fontSize = 12.sp)
Spacer(modifier = Modifier.height(12.dp))
Text("分数明细", color = GoldAccent, fontSize = 14.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(6.dp))
if (grouped.isEmpty()) {
Text("无明细数据", color = Color.White.copy(alpha = 0.4f), fontSize = 13.sp)
}
grouped.forEach { (cardLabel, count) ->
val cardScore = cardLabel.substringAfter("(").substringBefore("分)").toIntOrNull() ?: 0
val subtotal = cardScore * count
Row(modifier = Modifier.padding(vertical = 2.dp)) {
Text(
"$cardLabel × $count = ${subtotal}",
color = Color.White.copy(alpha = 0.75f),
fontSize = 13.sp
)
}
}
}
},
confirmButton = {
TextButton(onClick = { showDetailDialog = false }) {
Text("关闭", color = GoldAccent)
}
},
containerColor = DarkSurface
)
}
// Import dialog
if (showImportDialog) {
AlertDialog(
onDismissRequest = { showImportDialog = false },
title = { Text("导入备份", color = GoldAccent) },
text = {
Column {
Text("粘贴之前导出的JSON数据", color = Color.White.copy(alpha = 0.7f), fontSize = 14.sp)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = importJson,
onValueChange = { importJson = it },
modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedBorderColor = GoldAccent,
unfocusedBorderColor = Color.Gray,
cursorColor = GoldAccent
)
)
}
},
confirmButton = {
TextButton(onClick = {
try {
val imported = gson.fromJson(importJson, Array<ScoreEntry>::class.java)?.toList() ?: emptyList()
if (imported.isNotEmpty()) {
val merged = (scores + imported).distinctBy { "${it.name}_${it.mode}_${it.date}" }
.sortedByDescending { it.points }.take(50)
Scoreboard.saveScores(context, merged)
scores = merged
Toast.makeText(context, "成功导入 ${imported.size} 条记录", Toast.LENGTH_SHORT).show()
}
showImportDialog = false
importJson = ""
} catch (e: Exception) {
Toast.makeText(context, "JSON格式错误: ${e.message}", Toast.LENGTH_LONG).show()
}
}) { Text("导入", color = GoldAccent) }
},
dismissButton = {
TextButton(onClick = { showImportDialog = false; importJson = "" }) {
Text("取消", color = Color.White.copy(alpha = 0.5f))
}
},
containerColor = DarkSurface
)
}
} }