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 playerCount: Int,
val turnNumber: Int,
val date: Long
val date: Long,
val scoreDetail: String = ""
)
object Scoreboard {
@ -66,7 +67,8 @@ object Scoreboard {
difficulty: String,
duration: Int,
playerCount: Int,
turnNumber: Int
turnNumber: Int,
scoreDetail: String = ""
) {
val scores = loadScores(context).toMutableList()
scores.add(
@ -78,7 +80,8 @@ object Scoreboard {
duration = duration,
playerCount = playerCount,
turnNumber = turnNumber,
date = System.currentTimeMillis()
date = System.currentTimeMillis(),
scoreDetail = scoreDetail
)
)
saveScores(context, scores.sortedByDescending { it.points }.take(50))
@ -158,6 +161,15 @@ fun LocalGameScreen(
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(
context = context,
name = humanPlayerName,
@ -166,7 +178,8 @@ fun LocalGameScreen(
difficulty = gameDifficulty,
duration = gameDuration,
playerCount = totalPlayers,
turnNumber = gameTurnNumber
turnNumber = gameTurnNumber,
scoreDetail = detailStr
)
}
}
@ -315,7 +328,7 @@ fun LocalGameScreen(
gameLog.forEachIndexed { index, msg ->
Row(modifier = Modifier.padding(vertical = 2.dp)) {
Text(
"${index + 1}. ",
"${gameLog.size - index}. ",
color = Color.White.copy(alpha = 0.3f),
fontSize = 12.sp
)

View File

@ -38,6 +38,7 @@ fun MainMenuScreen(
onNameChanged: (String) -> Unit
) {
var playerName by remember { mutableStateOf(initialName) }
var showAbout by remember { mutableStateOf(false) }
Box(
modifier = Modifier
@ -225,6 +226,15 @@ fun MainMenuScreen(
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
Card(
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
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.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
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.unit.dp
import androidx.compose.ui.unit.sp
import com.google.gson.Gson
import com.unogame.game.GameMode
import com.unogame.ui.theme.*
import java.text.SimpleDateFormat
@ -28,10 +35,16 @@ import java.util.*
@Composable
fun ScoreboardScreen(onBack: () -> Unit) {
val context = LocalContext.current
val scores = remember { Scoreboard.loadScores(context) }
var scores by remember { mutableStateOf(Scoreboard.loadScores(context)) }
var selectedFilter by remember { mutableStateOf("全部") }
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
else scores.filter { it.mode == selectedFilter }
@ -49,6 +62,38 @@ fun ScoreboardScreen(onBack: () -> Unit) {
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
if (scores.isNotEmpty()) {
Row(
@ -106,7 +151,12 @@ fun ScoreboardScreen(onBack: () -> Unit) {
else "${entry.duration}"
Card(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.clickable {
detailEntry = entry
showDetailDialog = true
},
shape = RoundedCornerShape(12.dp),
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
)
}
}