Vibe coding全紀錄-54

A:

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

ui/navigation/*

​Routes.kt

​BottomNavItem.kt

​AppNavHost.kt

​ui/components/*

​BudgetSummaryCard.kt

​CategoryDialog.kt

​ParameterSlider.kt

​SegmentedButton.kt

​TransactionItem.kt

​TransactionTypeTabs.kt 

Q:

好的,我們繼續進行專案的完整程式碼回顧。

接下來的這部分是 App 的「骨架」與「積木」。navigation 套件定義了 App 的結構與畫面間的跳轉邏輯,是整個 UI 的骨架;components 套件則包含了所有我們精心打造、可在多個畫面重複使用的 UI 元件,是構成 App 視覺風格的積木。

以下是 ui/navigation/ui/components/ 兩個套件底下所有檔案的最終版程式碼。


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

套件: ui/navigation/

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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.navigation

/**
 * 將所有導航路線定義為常數,避免在程式碼中使用字串,方便管理與重構。
 */
object Routes {
    // Bottom Navigation Main Routes
    const val HOME = "home"
    const val BALANCE_SHEET = "balance_sheet"
    const val PLANNING = "planning"
    const val SETTINGS = "settings"

    // Sub-screens
    const val TRANSACTION_SCREEN = "transaction_screen" // For Add/Edit
    const val STATISTICS = "statistics"
    const val ACCOUNT_MANAGEMENT = "account_management"
    const val CATEGORY_MANAGEMENT = "category_management"
    const val PROJECT_LIST = "project_list"
    const val ADD_EDIT_PROJECT = "add_edit_project"
    const val PROJECT_DETAIL = "project_detail"
    const val LINK_TRANSACTIONS = "link_transactions"
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.navigation

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBalance
import androidx.compose.material.icons.filled.AutoGraph
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.vector.ImageVector

/**
 * 定義底部導覽列的每一個項目,包含其路線、圖示和標題。
 */
sealed class BottomNavItem(val route: String, val icon: ImageVector, val title: String) {
    object Transactions : BottomNavItem(Routes.HOME, Icons.Default.List, "明細")
    object BalanceSheet : BottomNavItem(Routes.BALANCE_SHEET, Icons.Default.AccountBalance, "資產")
    object Planning : BottomNavItem(Routes.PLANNING, Icons.Default.AutoGraph, "規劃")
    object Settings : BottomNavItem(Routes.SETTINGS, Icons.Default.Settings, "設定")
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.navigation

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.yourcompany.cashflow.ui.screens.*

/**
 * App 的導航主機,定義了所有畫面與其對應的路線。
 */
@Composable
fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
    NavHost(
        navController = navController,
        startDestination = BottomNavItem.Transactions.route,
        modifier = modifier
    ) {
        // --- Bottom Navigation Screens ---
        composable(BottomNavItem.Transactions.route) {
            HomeScreen(
                onNavigateToTransaction = { transactionId ->
                    navController.navigate("${Routes.TRANSACTION_SCREEN}?transactionId=$transactionId")
                },
                onNavigateToStatistics = { navController.navigate(Routes.STATISTICS) }
            )
        }
        composable(BottomNavItem.BalanceSheet.route) { BalanceSheetScreen() }
        composable(BottomNavItem.Planning.route) { BudgetPlanningScreen() }
        composable(BottomNavItem.Settings.route) {
            SettingsScreen(
                onNavigateToAccountManagement = { navController.navigate(Routes.ACCOUNT_MANAGEMENT) },
                onNavigateToCategoryManagement = { navController.navigate(Routes.CATEGORY_MANAGEMENT) },
                onNavigateToProjectManagement = { navController.navigate(Routes.PROJECT_LIST) }
            )
        }
        
        // --- Other Screens ---
        composable(Routes.STATISTICS) { StatisticsScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(Routes.ACCOUNT_MANAGEMENT) { AccountManagementScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(Routes.CATEGORY_MANAGEMENT) { CategoryManagementScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(Routes.PROJECT_LIST) {
            ProjectListScreen(
                onNavigateBack = { navController.popBackStack() },
                onNavigateToProject = { projectId ->
                    if (projectId == 0L) {
                        navController.navigate("${Routes.ADD_EDIT_PROJECT}?projectId=0")
                    } else {
                        navController.navigate("${Routes.PROJECT_DETAIL}/$projectId")
                    }
                }
            )
        }
        composable(
            route = "${Routes.PROJECT_DETAIL}/{projectId}",
            arguments = listOf(navArgument("projectId") { type = NavType.LongType })
        ) { backStackEntry ->
            val projectId = backStackEntry.arguments?.getLong("projectId") ?: 0L
            ProjectDetailScreen(
                projectId = projectId,
                onNavigateBack = { navController.popBackStack() },
                onNavigateToEditProject = { id -> navController.navigate("${Routes.ADD_EDIT_PROJECT}?projectId=$id") },
                onNavigateToLinkTransactions = { navController.navigate(Routes.LINK_TRANSACTIONS) }
            )
        }
        composable(
            route = "${Routes.ADD_EDIT_PROJECT}?projectId={projectId}",
            arguments = listOf(navArgument("projectId") { type = NavType.LongType; defaultValue = 0L })
        ) { backStackEntry ->
            val projectId = backStackEntry.arguments?.getLong("projectId") ?: 0L
            AddEditProjectScreen(
                projectId = projectId,
                onNavigateBack = { navController.popBackStack() }
            )
        }
        composable(Routes.LINK_TRANSACTIONS) { LinkTransactionsScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(
            route = "${Routes.TRANSACTION_SCREEN}?transactionId={transactionId}",
            arguments = listOf(navArgument("transactionId") { type = NavType.LongType; defaultValue = 0L })
        ) { backStackEntry ->
            val transactionId = backStackEntry.arguments?.getLong("transactionId") ?: 0L
            AddTransactionScreen(
                transactionId = transactionId,
                onNavigateBack = { navController.popBackStack() }
            )
        }
    }
}

</details>


套件: ui/components/

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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.yourcompany.cashflow.data.preferences.UserBudget

@Composable
fun BudgetSummaryCard(
    userBudget: UserBudget,
    currentMonthExpenses: Double
) {
    val remaining = userBudget.monthlyBudget - currentMonthExpenses
    val progress = if (userBudget.monthlyBudget > 0) (currentMonthExpenses / userBudget.monthlyBudget).toFloat() else 0f
    val isOverBudget = remaining < 0

    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("本月預算追蹤", style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(8.dp))
            Row(modifier = Modifier.fillMaxWidth()) {
                Text("剩餘", modifier = Modifier.weight(1f))
                Text(
                    "NT$ ${"%,.0f".format(remaining)}",
                    color = if (isOverBudget) MaterialTheme.colorScheme.error else Color.Unspecified,
                    fontWeight = FontWeight.Bold
                )
            }
            Spacer(modifier = Modifier.height(4.dp))
            LinearProgressIndicator(
                progress = { progress.coerceIn(0f, 1f) }, // Use lambda for state-derived values
                modifier = Modifier.fillMaxWidth(),
                color = if (isOverBudget) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
            )
            Spacer(modifier = Modifier.height(4.dp))
            Row(modifier = Modifier.fillMaxWidth()) {
                Text("支出: ${"%,.0f".format(currentMonthExpenses)}", style = MaterialTheme.typography.bodySmall)
                Spacer(modifier = Modifier.weight(1f))
                Text("預算: ${"%,.0f".format(userBudget.monthlyBudget)}", style = MaterialTheme.typography.bodySmall)
            }
        }
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import com.yourcompany.cashflow.data.local.entity.TransactionType
import com.yourcompany.cashflow.ui.screens.CategoryDialogState

@Composable
fun CategoryDialog(
    state: CategoryDialogState,
    onDismiss: () -> Unit,
    onConfirm: (name: String, type: TransactionType?) -> Unit
) {
    if (state is CategoryDialogState.Hidden) return

    var name by remember {
        mutableStateOf(
            when (state) {
                is CategoryDialogState.EditingMainCategory -> state.mainCategory.name
                is CategoryDialogState.EditingSubCategory -> state.subCategory.name
                else -> ""
            }
        )
    }

    var transactionType by remember {
        mutableStateOf(
            when (state) {
                is CategoryDialogState.AddingMainCategory -> state.type
                is CategoryDialogState.EditingMainCategory -> state.mainCategory.type
                else -> TransactionType.EXPENSE
            }
        )
    }
    
    // Ensure state resets when dialog reappears for a different purpose
    LaunchedEffect(state) {
        name = when (state) {
            is CategoryDialogState.EditingMainCategory -> state.mainCategory.name
            is CategoryDialogState.EditingSubCategory -> state.subCategory.name
            else -> ""
        }
        transactionType = when (state) {
            is CategoryDialogState.AddingMainCategory -> state.type
            is CategoryDialogState.EditingMainCategory -> state.mainCategory.type
            else -> TransactionType.EXPENSE
        }
    }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = {
            Text(
                when (state) {
                    is CategoryDialogState.AddingMainCategory -> "新增主分類"
                    is CategoryDialogState.EditingMainCategory -> "編輯主分類"
                    is CategoryDialogState.AddingSubCategory -> "新增子分類至 \"${state.parentCategory.name}\""
                    is CategoryDialogState.EditingSubCategory -> "編輯子分類"
                    is CategoryDialogState.Hidden -> ""
                }
            )
        },
        text = {
            Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                OutlinedTextField(
                    value = name,
                    onValueChange = { name = it },
                    label = { Text("名稱") }
                )
                if (state is CategoryDialogState.AddingMainCategory || state is CategoryDialogState.EditingMainCategory) {
                    TransactionTypeTabs(
                        selectedType = transactionType,
                        onTypeSelected = { transactionType = it }
                    )
                }
            }
        },
        confirmButton = {
            Button(
                onClick = {
                    if (name.isNotBlank()) {
                        val typeToConfirm = if (state is CategoryDialogState.AddingMainCategory || state is CategoryDialogState.EditingMainCategory) {
                            transactionType
                        } else {
                            null
                        }
                        onConfirm(name, typeToConfirm)
                    }
                }
            ) { Text("儲存") }
        },
        dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
    )
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight

@Composable
fun ParameterSlider(
    label: String,
    value: Float,
    onValueChange: (Float) -> Unit,
    valueRange: ClosedFloatingPointRange<Float>,
    steps: Int,
    formatValue: (Float) -> String
) {
    Column {
        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(label, style = MaterialTheme.typography.bodyLarge)
            Spacer(modifier = Modifier.weight(1f))
            Text(formatValue(value), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
        }
        Slider(
            value = value,
            onValueChange = onValueChange,
            valueRange = valueRange,
            steps = steps
        )
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SegmentedButton(
    modifier: Modifier = Modifier,
    options: List<String>,
    selectedIndex: Int,
    onSelectedIndex: (Int) -> Unit
) {
    SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) {
        options.forEachIndexed { index, label ->
            SegmentedButton(
                selected = index == selectedIndex,
                onClick = { onSelectedIndex(index) },
                shape = when (index) {
                    0 -> SegmentedButtonDefaults.itemShape(index = 0, count = options.size)
                    options.size - 1 -> SegmentedButtonDefaults.itemShape(index = index, count = options.size)
                    else -> SegmentedButtonDefaults.itemShape(index = index, count = options.size)
                }
            ) {
                Text(label)
            }
        }
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.unit.dp
import androidx.compose.ui.unit.sp
import com.yourcompany.cashflow.data.local.entity.TransactionType
import com.yourcompany.cashflow.data.model.TransactionWithDetails
import java.text.SimpleDateFormat
import java.util.*

@Composable
fun TransactionItem(
    transactionDetails: TransactionWithDetails,
    onClick: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .clickable(onClick = onClick),
        shape = RoundedCornerShape(8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Row(
            modifier = Modifier
                .padding(12.dp)
                .fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = transactionDetails.subCategory.name,
                    fontWeight = FontWeight.Bold,
                    fontSize = 16.sp
                )
                if (!transactionDetails.transaction.note.isNullOrEmpty()) {
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(
                        text = transactionDetails.transaction.note,
                        style = MaterialTheme.typography.bodySmall,
                        color = Color.Gray
                    )
                }
            }
            Column(horizontalAlignment = Alignment.End) {
                val amountColor = if (transactionDetails.transaction.type == TransactionType.INCOME)
                                    Color(0xFF2E7D32) // Dark Green
                                  else
                                    MaterialTheme.colorScheme.error

                Text(
                    text = "%,.0f".format(transactionDetails.transaction.amount),
                    color = amountColor,
                    fontWeight = FontWeight.SemiBold,
                    fontSize = 18.sp
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = SimpleDateFormat("MM/dd", Locale.getDefault()).format(Date(transactionDetails.transaction.transactionDate)),
                    style = MaterialTheme.typography.bodySmall,
                    color = Color.Gray
                )
            }
        }
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.yourcompany.cashflow.data.local.entity.TransactionType

@Composable
fun TransactionTypeTabs(
    selectedType: TransactionType,
    onTypeSelected: (TransactionType) -> Unit
) {
    val types = listOf("支出", "收入")
    val selectedIndex = if (selectedType == TransactionType.EXPENSE) 0 else 1

    TabRow(selectedTabIndex = selectedIndex) {
        types.forEachIndexed { index, title ->
            Tab(
                selected = selectedIndex == index,
                onClick = {
                    val type = if (index == 0) TransactionType.EXPENSE else TransactionType.INCOME
                    onTypeSelected(type)
                },
                text = { Text(text = title) }
            )
        }
    }
}

</details>


以上是我們專案 ui/navigation/ui/components/ 套件下所有檔案的最終版本。它們定義了 App 的脈絡和視覺基礎。