A:
請繼續提供剩餘的Screen檔案,謝謝
Q:
好的,我們繼續完成 ui/screens/ 套件的回顧。
第廿三步:最終程式碼回顧 (UI Screens 篇 - 續)
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/BalanceSheetScreen.kt
<details>
<summary><strong>點此展開 BalanceSheetScreen.kt 程式碼</strong></summary>
Kotlin
package com.yourcompany.cashflow.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.data.local.entity.Account
import com.yourcompany.cashflow.data.local.entity.AssetClass
import com.yourcompany.cashflow.data.model.AssetAllocation
import com.yourcompany.cashflow.data.model.MonthlyTotal
import com.yourcompany.cashflow.ui.viewmodel.BalanceSheetViewModel
import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.entryOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BalanceSheetScreen(
viewModel: BalanceSheetViewModel = hiltViewModel()
// onNavigateToAccountHistory: (Long) -> Unit // For future use
) {
val yearlyTrend by viewModel.yearlyAssetTrend.collectAsStateWithLifecycle()
val assetAllocation by viewModel.assetAllocation.collectAsStateWithLifecycle()
val accountsWithValue by viewModel.accountsWithLatestValue.collectAsStateWithLifecycle()
val allAccounts by viewModel.allAccounts.collectAsStateWithLifecycle()
var showUpdateDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(title = { Text("資產負債表") })
},
floatingActionButton = {
FloatingActionButton(onClick = { showUpdateDialog = true }) {
Icon(Icons.Default.Add, "更新本月餘額")
}
}
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item { YearlyTrendChart(data = yearlyTrend) }
item { AssetAllocationSection(data = assetAllocation) }
item { Text("帳戶列表", style = MaterialTheme.typography.titleMedium) }
items(accountsWithValue) { accountItem ->
ListItem(
headlineContent = { Text(accountItem.account.name) },
supportingContent = { Text(accountItem.account.assetClass.name) },
trailingContent = {
Text(
text = "NT$ ${"%,.0f".format(accountItem.latestValue ?: 0.0)}",
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyLarge
)
}
)
Divider()
}
}
}
if (showUpdateDialog) {
UpdateBalancesDialog(
accounts = allAccounts,
onDismiss = { showUpdateDialog = false },
onConfirm = { updates ->
updates.forEach { (accountId, value) ->
viewModel.addOrUpdateSnapshot(accountId, value)
}
showUpdateDialog = false
}
)
}
}
@Composable
fun YearlyTrendChart(data: List<MonthlyTotal>) {
val chartProducer = remember { ChartEntryModelProducer() }
val entries = data.mapIndexed { index, monthlyTotal -> entryOf(index.toFloat(), monthlyTotal.totalValue.toFloat()) }
chartProducer.setEntries(entries)
val bottomAxisFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, _ ->
val month = data.getOrNull(value.toInt())?.yearMonth?.substring(5, 7) ?: ""
"${month}月"
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("年度資產趨勢", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(16.dp))
if (entries.isNotEmpty()) {
Chart(
chart = columnChart(),
chartModelProducer = chartProducer,
startAxis = rememberStartAxis(),
bottomAxis = rememberBottomAxis(valueFormatter = bottomAxisFormatter),
modifier = Modifier.height(200.dp)
)
} else {
Box(modifier = Modifier.height(200.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
Text("尚無資料可繪製趨勢圖")
}
}
}
}
}
@Composable
fun AssetAllocationSection(data: List<AssetAllocation>) {
val totalAssets = data.sumOf { it.totalValue }
val colorMap = mapOf(
AssetClass.CASH to Color(0xFF66BB6A),
AssetClass.STOCK to Color(0xFF42A5F5),
AssetClass.BOND to Color(0xFFFFA726),
AssetClass.OTHER to Color(0xFF8D6E63)
)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("資產配置比例", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(16.dp))
if (totalAssets > 0) {
data.forEach { allocation ->
val percentage = (allocation.totalValue / totalAssets * 100)
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(modifier = Modifier.size(16.dp).background(colorMap[allocation.assetClass] ?: Color.Gray))
Spacer(modifier = Modifier.width(8.dp))
Text(text = allocation.assetClass.name, modifier = Modifier.weight(1f))
Text(text = "NT$ ${"%,.0f".format(allocation.totalValue)}", modifier = Modifier.weight(1.5f), textAlign = TextAlign.End)
Text(text = "%.1f%%".format(percentage), modifier = Modifier.weight(1f), textAlign = TextAlign.End)
}
}
} else {
Box(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), contentAlignment = Alignment.Center) {
Text("尚無資產可進行配置分析")
}
}
}
}
}
@Composable
fun UpdateBalancesDialog(
accounts: List<Account>,
onDismiss: () -> Unit,
onConfirm: (updates: Map<Long, Double>) -> Unit
) {
var inputValues by remember { mutableStateOf<Map<Long, String>>(emptyMap()) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("更新本月餘額") },
text = {
LazyColumn {
items(accounts) { account ->
OutlinedTextField(
value = inputValues[account.id] ?: "",
onValueChange = { newValue ->
if (newValue.all { it.isDigit() }) {
inputValues = inputValues + (account.id to newValue)
}
},
label = { Text(account.name) },
modifier = Modifier.padding(vertical = 4.dp)
)
}
}
},
confirmButton = {
Button(onClick = {
val updates = inputValues.mapValues { it.value.toDoubleOrNull() }
.filterValues { it != null }
.mapValues { it.value!! }
onConfirm(updates)
}) { Text("儲存") }
},
dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
)
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/BudgetPlanningScreen.kt
<details>
<summary><strong>點此展開 BudgetPlanningScreen.kt 程式碼</strong></summary>
Kotlin
package com.yourcompany.cashflow.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.ui.components.ParameterSlider
import com.yourcompany.cashflow.ui.viewmodel.BudgetViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BudgetPlanningScreen(
viewModel: BudgetViewModel = hiltViewModel()
) {
val plannedReturnRate by viewModel.plannedReturnRate.collectAsStateWithLifecycle()
val inflationRate by viewModel.inflationRate.collectAsStateWithLifecycle()
val remainingYears by viewModel.remainingYears.collectAsStateWithLifecycle()
val annualBudget by viewModel.annualDisposableBudget.collectAsStateWithLifecycle()
val netWorth by viewModel.latestNetWorth.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar(title = { Text("年度預算規劃") })
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
ResultCard(
annualBudget = annualBudget,
currentNetWorth = netWorth?.value ?: 0.0
)
}
item {
ParametersCard(
returnRate = plannedReturnRate,
onReturnRateChange = { viewModel.setPlannedReturnRate(it) },
inflationRate = inflationRate,
onInflationRateChange = { viewModel.setInflationRate(it) },
years = remainingYears.toFloat(),
onYearsChange = { viewModel.setRemainingYears(it.toInt()) }
)
}
item {
Button(
onClick = {
viewModel.saveBudget()
scope.launch {
snackbarHostState.showSnackbar("預算已成功儲存!")
}
},
modifier = Modifier.fillMaxWidth(),
enabled = annualBudget > 0
) {
Text("儲存為本年度預算")
}
}
item {
Text(
"提示:此計算基於 PMT 模型,結果僅為財務參考,實際市場狀況可能有所不同。",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
fun ResultCard(annualBudget: Double, currentNetWorth: Double) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("年度可支配預算", style = MaterialTheme.typography.titleMedium)
Text(
text = "NT$ ${"%,.0f".format(annualBudget)}",
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "每月平均 NT$ ${"%,.0f".format(annualBudget / 12)}",
style = MaterialTheme.typography.bodyLarge
)
Divider(modifier = Modifier.padding(vertical = 8.dp))
Text("計算基礎:當前總淨值 NT$ ${"%,.0f".format(currentNetWorth)}")
}
}
}
@Composable
fun ParametersCard(
returnRate: Float, onReturnRateChange: (Float) -> Unit,
inflationRate: Float, onInflationRateChange: (Float) -> Unit,
years: Float, onYearsChange: (Float) -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("規劃參數", style = MaterialTheme.typography.titleLarge)
ParameterSlider(
label = "規劃報酬率", value = returnRate, onValueChange = onReturnRateChange,
valueRange = 0f..0.15f, steps = 14, formatValue = { "%.1f %%".format(it * 100) }
)
ParameterSlider(
label = "通貨膨脹率", value = inflationRate, onValueChange = onInflationRateChange,
valueRange = 0f..0.05f, steps = 9, formatValue = { "%.1f %%".format(it * 100) }
)
ParameterSlider(
label = "剩餘年數", value = years, onValueChange = onYearsChange,
valueRange = 5f..50f, steps = 44, formatValue = { "%.0f 年".format(it) }
)
}
}
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/SettingsScreen.kt
<details>
<summary><strong>點此展開 SettingsScreen.kt 程式碼</strong></summary>
Kotlin
package com.yourcompany.cashflow.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onNavigateToAccountManagement: () -> Unit,
onNavigateToCategoryManagement: () -> Unit,
onNavigateToProjectManagement: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("設定") })
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
ListItem(
headlineContent = { Text("管理帳戶") },
modifier = Modifier.clickable(onClick = onNavigateToAccountManagement)
)
Divider()
ListItem(
headlineContent = { Text("管理分類") },
modifier = Modifier.clickable(onClick = onNavigateToCategoryManagement)
)
Divider()
ListItem(
headlineContent = { Text("管理專案") },
modifier = Modifier.clickable(onClick = onNavigateToProjectManagement)
)
Divider()
}
}
}
</details>
(由於檔案數量和程式碼長度非常多,為了避免回應過於龐大,我將在下一個回應中繼續提供剩餘的 Screen 檔案。)