Vibe coding全紀錄-55

A:

要請幫忙一步一步詳細仔細的列出最終版的下列程式碼,謝謝

​ui/screens/*

​MainScreen.kt (App 主框架)

​HomeScreen.kt

​StatisticsScreen.kt

​BalanceSheetScreen.kt

​BudgetPlanningScreen.kt

​SettingsScreen.kt

​AccountManagementScreen.kt

​CategoryManagementScreen.kt

​ProjectListScreen.kt

​ProjectDetailScreen.kt

​AddEditProjectScreen.kt

​LinkTransactionsScreen.kt

​AddTransactionScreen.kt 

Q:

好的,我們來進行這次完整程式碼回顧的最後一個,也是內容最豐富的部分。

screens 套件是我們 App 的心臟地帶,它將所有後端邏輯 (ViewModel)、數據 (StateFlows) 和可重複使用的積木 (Components) 組合成一個個使用者可以實際看到並互動的功能頁面。

以下是 ui/screens/ 套件底下所有畫面 Composable 的最終版程式碼。


第廿三步:最終程式碼回顧 (UI Screens 篇)

檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/MainScreen.kt

<details>

<summary><strong>點此展開 MainScreen.kt 程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.yourcompany.cashflow.ui.navigation.AppNavHost
import com.yourcompany.cashflow.ui.navigation.BottomNavItem

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
    val navController = rememberNavController()
    Scaffold(
        bottomBar = { BottomNavigationBar(navController = navController) }
    ) { innerPadding ->
        AppNavHost(
            navController = navController,
            modifier = Modifier.padding(innerPadding)
        )
    }
}

@Composable
fun BottomNavigationBar(navController: NavController) {
    val items = listOf(
        BottomNavItem.Transactions,
        BottomNavItem.BalanceSheet,
        BottomNavItem.Planning,
        BottomNavItem.Settings
    )
    NavigationBar {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route

        items.forEach { item ->
            NavigationBarItem(
                icon = { Icon(item.icon, contentDescription = item.title) },
                label = { Text(item.title) },
                selected = currentRoute == item.route,
                onClick = {
                    navController.navigate(item.route) {
                        navController.graph.startDestinationRoute?.let { route ->
                            popUpTo(route) { saveState = true }
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }
}

</details>


檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/HomeScreen.kt

<details>

<summary><strong>點此展開 HomeScreen.kt 程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
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.material.icons.filled.PieChart
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.ui.components.BudgetSummaryCard
import com.yourcompany.cashflow.ui.components.TransactionItem
import com.yourcompany.cashflow.ui.viewmodel.TransactionViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    viewModel: TransactionViewModel = hiltViewModel(),
    onNavigateToTransaction: (Long) -> Unit,
    onNavigateToStatistics: () -> Unit
) {
    val transactions by viewModel.allTransactions.collectAsStateWithLifecycle()
    val userBudget by viewModel.userBudget.collectAsStateWithLifecycle()
    val currentMonthExpenses by viewModel.currentMonthExpenses.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("交易明細") },
                actions = {
                    IconButton(onClick = onNavigateToStatistics) {
                        Icon(Icons.Default.PieChart, contentDescription = "統計")
                    }
                }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { onNavigateToTransaction(0L) }) {
                Icon(Icons.Default.Add, contentDescription = "新增交易")
            }
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            contentPadding = PaddingValues(8.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            // 只在使用者設定過預算時,才顯示卡片
            if (userBudget.annualBudget > 0) {
                item {
                    Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
                        BudgetSummaryCard(
                            userBudget = userBudget,
                            currentMonthExpenses = currentMonthExpenses
                        )
                    }
                }
            }

            items(transactions) { transactionDetail ->
                Box(modifier = Modifier.padding(horizontal = 8.dp)) {
                    TransactionItem(
                        transactionDetails = transactionDetail,
                        onClick = { onNavigateToTransaction(transactionDetail.transaction.id) }
                    )
                }
            }
        }
    }
}

</details>


檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/StatisticsScreen.kt

<details>

<summary><strong>點此展開 StatisticsScreen.kt 程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
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.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.model.CategorySum
import com.yourcompany.cashflow.data.model.SubCategorySum
import com.yourcompany.cashflow.ui.components.SegmentedButton
import com.yourcompany.cashflow.ui.components.TransactionTypeTabs
import com.yourcompany.cashflow.ui.viewmodel.StatisticsViewModel
import com.yourcompany.cashflow.ui.viewmodel.StatisticsViewMode
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 StatisticsScreen(
    viewModel: StatisticsViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit
) {
    val viewMode by viewModel.viewMode.collectAsStateWithLifecycle()
    val transactionType by viewModel.transactionType.collectAsStateWithLifecycle()
    val mainChartData by viewModel.mainChartData.collectAsStateWithLifecycle()
    val selectedProject by viewModel.selectedProject.collectAsStateWithLifecycle()
    val drillDownLevel1Data by viewModel.drillDownLevel1Data.collectAsStateWithLifecycle()
    val selectedMainCategoryInProject by viewModel.selectedMainCategoryInProject.collectAsStateWithLifecycle()
    val drillDownLevel2Data by viewModel.drillDownLevel2Data.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("財務統計") },
                navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } }
            )
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            contentPadding = PaddingValues(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            item {
                SegmentedButton(
                    options = listOf("依分類", "依專案"),
                    selectedIndex = viewMode.ordinal,
                    onSelectedIndex = { index ->
                        viewModel.setViewMode(if (index == 0) StatisticsViewMode.BY_CATEGORY else StatisticsViewMode.BY_PROJECT)
                    }
                )
            }

            item {
                MainChartCard(
                    title = if (viewMode == StatisticsViewMode.BY_CATEGORY) "主分類消費統計" else "專案消費統計",
                    data = mainChartData,
                    transactionType = transactionType,
                    selectedItem = if (viewMode == StatisticsViewMode.BY_PROJECT) selectedProject else null, // Only highlight in project mode
                    onTransactionTypeChange = { viewModel.setTransactionType(it) },
                    onItemClick = { viewModel.selectItem(it) }
                )
            }
            
            item {
                AnimatedVisibility(visible = drillDownLevel1Data.isNotEmpty() && selectedProject != null) {
                    DrillDownCard(
                        title = "『${selectedProject?.categoryName}』內部分類",
                        data = drillDownLevel1Data,
                        selectedItem = selectedMainCategoryInProject,
                        onItemClick = { viewModel.selectDrillDownLevel1Item(it) }
                    )
                }
            }

            item {
                AnimatedVisibility(visible = drillDownLevel2Data.isNotEmpty() && selectedMainCategoryInProject != null) {
                    SubCategoryAllocationSection(
                        title = "『${selectedMainCategoryInProject?.categoryName}』消費佔比",
                        data = drillDownLevel2Data
                    )
                }
            }
        }
    }
}


@Composable
fun MainChartCard(
    title: String,
    data: List<CategorySum>,
    transactionType: com.yourcompany.cashflow.data.local.entity.TransactionType,
    selectedItem: CategorySum?,
    onTransactionTypeChange: (com.yourcompany.cashflow.data.local.entity.TransactionType) -> Unit,
    onItemClick: (CategorySum) -> Unit
) {
    val chartProducer = remember { ChartEntryModelProducer() }
    val entries = data.mapIndexed { index, categorySum -> entryOf(index.toFloat(), categorySum.amount.toFloat()) }
    chartProducer.setEntries(entries)

    val bottomAxisFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, _ ->
        data.getOrNull(value.toInt())?.categoryName ?: ""
    }
    
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(title, style = MaterialTheme.typography.titleLarge)
            TransactionTypeTabs(
                selectedType = transactionType,
                onTypeSelected = onTransactionTypeChange
            )
            Spacer(modifier = Modifier.height(16.dp))
            if (entries.isNotEmpty()) {
                Chart(
                    chart = columnChart(),
                    chartModelProducer = chartProducer,
                    startAxis = rememberStartAxis(),
                    bottomAxis = rememberBottomAxis(
                        valueFormatter = bottomAxisFormatter,
                        labelRotationDegrees = 45f
                    ),
                    modifier = Modifier.height(250.dp)
                )
                Divider(modifier = Modifier.padding(vertical = 8.dp))
                data.forEach { item ->
                    ListItem(
                        headlineText = { Text(item.categoryName) },
                        trailingContent = { Text("NT$ ${"%,.0f".format(item.amount)}") },
                        modifier = Modifier.clickable { onItemClick(item) },
                        colors = if (item == selectedItem) {
                            ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.primaryContainer)
                        } else {
                            ListItemDefaults.colors()
                        }
                    )
                }
            } else {
                Box(modifier = Modifier.height(250.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
                    Text("本月尚無相關紀錄")
                }
            }
        }
    }
}

@Composable
fun DrillDownCard(
    title: String,
    data: List<CategorySum>,
    selectedItem: CategorySum?,
    onItemClick: (CategorySum) -> Unit
) {
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(8.dp))
            data.forEach { item ->
                ListItem(
                    headlineText = { Text(item.categoryName) },
                    trailingContent = { Text("NT$ ${"%,.0f".format(item.amount)}") },
                    modifier = Modifier.clickable { onItemClick(item) },
                    colors = if (item == selectedItem) {
                        ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
                    } else {
                        ListItemDefaults.colors()
                    }
                )
            }
        }
    }
}

@Composable
fun SubCategoryAllocationSection(title: String, data: List<SubCategorySum>) {
    val totalAmount = data.sumOf { it.amount }

    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(16.dp))
            data.forEach { subCategory ->
                val percentage = (subCategory.amount / totalAmount * 100)
                Row(
                    modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Text(text = subCategory.subCategoryName, modifier = Modifier.weight(1f))
                    LinearProgressIndicator(
                        progress = { percentage.toFloat() / 100f },
                        modifier = Modifier.weight(2f).padding(horizontal = 8.dp)
                    )
                    Text(text = "%.1f%%".format(percentage), modifier = Modifier.weight(1f), textAlign = TextAlign.End)
                }
            }
        }
    }
}

</details>


(由於檔案數量和程式碼長度非常多,為了避免回應過於龐大,我將在下一個回應中繼續提供剩餘的 Screen 檔案。)