From cf9bc941487df8dfb0780d3e8f1281f4e397b5fa Mon Sep 17 00:00:00 2001
From: cloudroam <cloudroam>
Date: 星期四, 06 三月 2025 10:33:57 +0800
Subject: [PATCH] fix: 3

---
 app/src/main/java/com/example/firstapp/model/DailyStat.kt                    |    2 
 app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt |   26 ++
 app/src/main/res/layout/layout_year_stats.xml                                |   61 +++++
 app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt     |  304 ++++++++++++++++++++------
 app/src/main/res/drawable/ic_average.xml                                     |   10 
 app/src/main/res/layout/fragment_dashboard.xml                               |   17 +
 app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt               |  162 ++++++++++++--
 app/src/main/res/layout/layout_week_stats.xml                                |   31 ++
 app/src/main/java/com/example/firstapp/model/HeatmapStat.kt                  |    7 
 app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt    |   15 +
 app/src/main/res/drawable/ic_package.xml                                     |   10 
 app/src/main/res/values/dimens.xml                                           |    1 
 12 files changed, 531 insertions(+), 115 deletions(-)

diff --git a/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt b/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt
index 3f5d7ae..39d54e2 100644
--- a/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt
+++ b/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt
@@ -9,6 +9,7 @@
 import com.example.firstapp.database.entity.Msg
 import com.example.firstapp.model.CourierStat
 import com.example.firstapp.model.DailyStat
+import com.example.firstapp.model.HeatmapStat
 import io.reactivex.Completable
 import kotlinx.coroutines.flow.Flow
 
@@ -83,30 +84,28 @@
     fun getCourierStatsByWeek(date: Long): Flow<List<CourierStat>>
 
     @Query("""
-        WITH RECURSIVE weeks(week_start, week_end) AS (
-            -- 从选定的日期开始计算周
-            SELECT 
-                date(datetime(:date/1000, 'unixepoch', 'localtime'), 'weekday 1', '-28 days') as week_start,
-                date(datetime(:date/1000, 'unixepoch', 'localtime'), 'weekday 0', '-28 days') as week_end
-            UNION ALL
-            SELECT 
-                date(week_start, '+7 days'),
-                date(week_end, '+7 days')
-            FROM weeks
-            WHERE date(week_start) <= date(datetime(:date/1000, 'unixepoch', 'localtime'))
-            LIMIT 5
-        )
-        SELECT 
-            CAST(strftime('%W', week_start) AS INTEGER) as date,
-            COUNT(code.id) as count,
-            week_start as week_start
-        FROM weeks 
-        LEFT JOIN code ON datetime(code.createtime) BETWEEN datetime(weeks.week_start) 
-            AND datetime(weeks.week_end, '+23 hours', '+59 minutes', '+59 seconds')
-        GROUP BY weeks.week_start
-        ORDER BY weeks.week_start ASC
-    """)
-    fun getDailyStatsByWeek(date: Long): Flow<List<DailyStat>>
+    WITH RECURSIVE dates(date_value) AS (
+        -- 从当前日期往前3周的周一开始
+        SELECT date(datetime(:date/1000, 'unixepoch', 'localtime'), 'weekday 0', '-21 days') as date_value
+        UNION ALL
+        -- 每次加7天,直到后2周
+        SELECT date(date_value, '+7 days')
+        FROM dates
+        WHERE date_value < date(datetime(:date/1000, 'unixepoch', 'localtime'), 'weekday 0', '+14 days')
+    )
+    SELECT 
+        strftime('%Y-%m-%d', date_value) as date,
+        COUNT(c.id) as count,
+        date_value as weekStart
+    FROM dates d
+    LEFT JOIN code c ON strftime('%Y-%m-%d', c.createtime) BETWEEN 
+        strftime('%Y-%m-%d', d.date_value) 
+        AND strftime('%Y-%m-%d', date(d.date_value, '+6 days'))
+    GROUP BY d.date_value
+    ORDER BY d.date_value ASC
+    LIMIT :weekCount
+""")
+    fun getWeeklyStats(date: Long, weekCount: Int): Flow<List<DailyStat>>
 
     @Query("""
         SELECT * FROM code 
@@ -115,4 +114,119 @@
         ORDER BY createtime DESC
     """)
      fun getPackagesByWeek(date: Long): Flow<List<Code>>
+
+    @Query("""
+        SELECT type as courierName, COUNT(*) as count 
+        FROM code 
+        WHERE strftime('%Y-%m', substr(createtime, 1, 10)) = 
+              strftime('%Y-%m', datetime(:date/1000, 'unixepoch', 'localtime'))
+        GROUP BY type
+        ORDER BY count DESC
+    """)
+    fun getCourierStatsByMonth(date: Long): Flow<List<CourierStat>>
+
+    @Query("""
+        SELECT type as courierName, COUNT(*) as count 
+        FROM code 
+        WHERE strftime('%Y', substr(createtime, 1, 10)) = 
+              strftime('%Y', datetime(:date/1000, 'unixepoch', 'localtime'))
+        GROUP BY type
+        ORDER BY count DESC
+    """)
+    fun getCourierStatsByYear(date: Long): Flow<List<CourierStat>>
+
+//    @Query("""
+//        WITH RECURSIVE months(month_start) AS (
+//            SELECT date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of month', '-11 months')
+//            UNION ALL
+//            SELECT date(month_start, '+1 month')
+//            FROM months
+//            WHERE date(month_start) < date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of month')
+//        )
+//        SELECT
+//            strftime('%m', month_start) as date,
+//            COUNT(code.id) as count,
+//            month_start as week_start
+//        FROM months
+//        LEFT JOIN code ON strftime('%Y-%m', code.createtime) = strftime('%Y-%m', months.month_start)
+//        GROUP BY months.month_start
+//        ORDER BY months.month_start ASC
+//    """)
+//    fun getMonthlyStats(date: Long): Flow<List<DailyStat>>
+
+    @Query("""
+        WITH RECURSIVE years(year_start) AS (
+            SELECT date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of year', '-4 years')
+            UNION ALL
+            SELECT date(year_start, '+1 year')
+            FROM years
+            WHERE date(year_start) < date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of year')
+        )
+        SELECT 
+            strftime('%Y', year_start) as date,
+            COUNT(code.id) as count,
+            year_start as week_start
+        FROM years 
+        LEFT JOIN code ON strftime('%Y', code.createtime) = strftime('%Y', years.year_start)
+        GROUP BY years.year_start
+        ORDER BY years.year_start ASC
+    """)
+    fun getYearlyStats(date: Long): Flow<List<DailyStat>>
+
+    @Query("""
+        WITH RECURSIVE dates(date) AS (
+            SELECT date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of year')
+            UNION ALL
+            SELECT date(date, '+1 day')
+            FROM dates
+            WHERE date < date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of year', '+1 year', '-1 day')
+        )
+        SELECT 
+            CAST(strftime('%w', dates.date) AS INTEGER) as dayOfWeek,
+            CAST(strftime('%W', dates.date) AS INTEGER) as weekOfYear,
+            COUNT(code.id) as count
+        FROM dates 
+        LEFT JOIN code ON date(code.createtime) = dates.date
+        GROUP BY dates.date
+        ORDER BY dates.date
+    """)
+    fun getYearlyHeatmap(date: Long): Flow<List<HeatmapStat>>
+
+    @Query("""
+        WITH RECURSIVE days(date) AS (
+            SELECT date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of month')
+            UNION ALL
+            SELECT date(date, '+1 day')
+            FROM days
+            WHERE date < date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of month', '+1 month', '-1 day')
+        )
+        SELECT 
+            strftime('%d', days.date) || '' as date,  -- 确保 date 不为空
+            COUNT(code.id) as count,
+            days.date as week_start
+        FROM days 
+        LEFT JOIN code ON date(code.createtime) = days.date
+        GROUP BY days.date
+        ORDER BY days.date ASC
+    """)
+    fun getMonthlyStats(date: Long): Flow<List<DailyStat>>
+
+    @Query("""
+        WITH RECURSIVE months(month_start) AS (
+            SELECT date(datetime(:date/1000, 'unixepoch', 'localtime'), 'start of year') as month_start
+            UNION ALL
+            SELECT date(month_start, '+1 month')
+            FROM months
+            WHERE strftime('%m', month_start) < '12'
+        )
+        SELECT 
+            strftime('%m', month_start) as date,
+            COUNT(code.id) as count,
+            month_start as weekStart
+        FROM months 
+        LEFT JOIN code ON strftime('%Y-%m', code.createtime) = strftime('%Y-%m', months.month_start)
+        GROUP BY months.month_start
+        ORDER BY months.month_start ASC
+    """)
+    fun getYearMonthlyStats(date: Long): Flow<List<DailyStat>>
 }
diff --git a/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt b/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt
index f187d22..915c13d 100644
--- a/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt
+++ b/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt
@@ -3,9 +3,7 @@
 import androidx.annotation.WorkerThread
 import com.example.firstapp.database.dao.CodeDao
 import com.example.firstapp.database.entity.Code
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.withContext
 
 
 class CodeRepository(private val codeDao: CodeDao) {
@@ -41,9 +39,19 @@
         }
     }
 
-    fun getCourierStats(date: Long) = codeDao.getCourierStatsByWeek(date)
+    fun getCourierStats(date: Long, dateType: String) = when(dateType) {
+        "WEEK" -> codeDao.getCourierStatsByWeek(date)
+        "MONTH" -> codeDao.getCourierStatsByMonth(date)
+        "YEAR" -> codeDao.getCourierStatsByYear(date)
+        else -> codeDao.getCourierStatsByWeek(date)
+    }
 
-    fun getDailyStats(date: Long) = codeDao.getDailyStatsByWeek(date)
+//    fun getStats(date: Long, dateType: String) = when(dateType) {
+//        "WEEK" -> codeDao.getDailyStatsByWeek(date)
+//        "MONTH" -> codeDao.getMonthlyStats(date)
+//        "YEAR" -> codeDao.getYearlyStats(date)
+//        else -> codeDao.getDailyStatsByWeek(date)
+//    }
 
 
     @WorkerThread
@@ -51,5 +59,15 @@
         return codeDao.getNewPackagesByDay(date)
     }
 
+    fun getYearlyHeatmap(date: Long) = codeDao.getYearlyHeatmap(date)
+
+    fun getWeeklyStats(date: Long, weekCount: Int) = codeDao.getWeeklyStats(date, weekCount)
+    
+    fun getMonthlyStats(date: Long) = codeDao.getMonthlyStats(date)
+
+
+    fun getYearlyStats(date: Long) = codeDao.getYearlyStats(date)
+
+    fun getYearMonthlyStats(date: Long) = codeDao.getYearMonthlyStats(date)
 
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/model/DailyStat.kt b/app/src/main/java/com/example/firstapp/model/DailyStat.kt
index 0f2d8d8..b6c7301 100644
--- a/app/src/main/java/com/example/firstapp/model/DailyStat.kt
+++ b/app/src/main/java/com/example/firstapp/model/DailyStat.kt
@@ -13,5 +13,5 @@
 data class DailyStat(
     val date: String,
     val count: Int,
-    val week_start: String? = null
+    val weekStart: Long? = null
 )
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/model/HeatmapStat.kt b/app/src/main/java/com/example/firstapp/model/HeatmapStat.kt
new file mode 100644
index 0000000..4d04b5c
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/model/HeatmapStat.kt
@@ -0,0 +1,7 @@
+package com.example.firstapp.model
+
+data class HeatmapStat(
+    val dayOfWeek: Int,  // 0-6 (周日-周六)
+    val weekOfYear: Int, // 0-51
+    val count: Int
+) 
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
index bfd5bb8..cecb616 100644
--- a/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
+++ b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
@@ -7,37 +7,33 @@
 import android.view.ViewGroup
 import android.widget.TextView
 import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
 import androidx.fragment.app.viewModels
 import com.example.firstapp.databinding.FragmentDashboardBinding
 import com.google.android.material.tabs.TabLayout
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.example.firstapp.adapter.PackageAdapter
-import com.example.firstapp.core.Core
 import com.github.mikephil.charting.charts.BarChart
 import com.github.mikephil.charting.charts.PieChart
 import com.github.mikephil.charting.components.Legend
 import com.github.mikephil.charting.components.XAxis
 import com.github.mikephil.charting.data.*
-import com.github.mikephil.charting.formatter.IndexAxisValueFormatter
 import com.github.mikephil.charting.formatter.ValueFormatter
 import java.util.*
 import java.text.SimpleDateFormat
-import android.util.Log
+import android.graphics.Color
+import android.widget.GridLayout
+import com.example.firstapp.model.DailyStat
 
 class DashboardFragment : Fragment() {
 
     private var _binding: FragmentDashboardBinding? = null
-
-    // This property is only valid between onCreateView and
-    // onDestroyView.
     private val binding get() = _binding!!
-
     private val packageAdapter = PackageAdapter()
     private var currentDate = Calendar.getInstance()
     private var currentDateType = DateType.DAY
     private lateinit var barChart: BarChart
     private lateinit var pieChart: PieChart
+    private lateinit var heatmapView: View
     enum class DateType {
         DAY, WEEK, MONTH, YEAR
     }
@@ -55,11 +51,14 @@
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
+        //渲染包裹列表
         setupRecyclerView()
+        //初始化tab内容和数据
         setupTabLayout()
+        //日期调整
         setupDatePicker()
+        setupView(view)
         updateDateDisplay()
-        setupWeekView(view)
         loadPackages()
     }
 
@@ -80,16 +79,9 @@
                     3 -> DateType.YEAR
                     else -> DateType.DAY
                 }
-                // 切换视图
-                binding.viewFlipperStats.displayedChild = when(currentDateType) {
-                    DateType.DAY -> 0
-                    DateType.WEEK -> 1
-                    else -> 0
-                }
                 updateDateDisplay()
-                //加载按天统计包裹数量和列表
+                updateCharts()
                 loadPackages()
-                observePackages()
             }
             override fun onTabUnselected(tab: TabLayout.Tab?) {}
             override fun onTabReselected(tab: TabLayout.Tab?) {}
@@ -113,25 +105,48 @@
             DateType.YEAR -> currentDate.add(Calendar.YEAR, amount)
         }
         updateDateDisplay()
+        updateCharts()
         loadPackages()
     }
 
     private fun updateDateDisplay() {
         val dateFormat = when (currentDateType) {
             DateType.DAY -> "yyyy年MM月dd日"
-            DateType.WEEK -> "yyyy年第ww周"
+            DateType.WEEK -> {
+                // 获取本周的起始和结束日期
+                val calendar = currentDate.clone() as Calendar
+                calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
+                val startDate = SimpleDateFormat("MM月dd日", Locale.getDefault()).format(calendar.time)
+                
+                calendar.add(Calendar.DAY_OF_WEEK, 6)
+                val endDate = SimpleDateFormat("MM月dd日", Locale.getDefault()).format(calendar.time)
+                
+                "$startDate-$endDate"
+            }
             DateType.MONTH -> "yyyy年MM月"
             DateType.YEAR -> "yyyy年"
         }
-        binding.textCurrentDate.text = SimpleDateFormat(dateFormat, Locale.getDefault())
-            .format(currentDate.time)
+        
+        if (currentDateType == DateType.WEEK) {
+            binding.textCurrentDate.text = dateFormat
+        } else {
+            binding.textCurrentDate.text = SimpleDateFormat(dateFormat, Locale.getDefault())
+                .format(currentDate.time)
+        }
     }
-    private fun setupWeekView(view: View) {
+    private fun setupView(view: View) {
         val weekStatsView = binding.layoutWeekStats.root
         barChart = weekStatsView.findViewById(R.id.chart_daily_packages)
         pieChart = weekStatsView.findViewById(R.id.chart_courier_distribution)
+        heatmapView = weekStatsView.findViewById(R.id.heatmap_yearly)
+        
+        // 初始化时隐藏统计视图
+        weekStatsView.visibility = View.GONE
+        
         setupBarChart()
         setupPieChart()
+        setupHeatmap()
+        updateCharts()
     }
     private fun setupBarChart() {
         barChart.apply {
@@ -155,10 +170,15 @@
             axisLeft.apply {
                 setDrawGridLines(true)
                 axisMinimum = 0f
-                granularity = 1f
+                granularity = 0.5f  // 将刻度间隔设为0.5
                 valueFormatter = object : ValueFormatter() {
                     override fun getFormattedValue(value: Float): String {
-                        return value.toInt().toString()
+                        // 只有整数时才显示标签
+                        return if (value % 1 == 0f) {
+                            value.toInt().toString()
+                        } else {
+                            ""
+                        }
                     }
                 }
             }
@@ -173,7 +193,17 @@
         updateBarChartData()
     }
     private fun updateBarChartData() {
-        viewModel.getDailyStats(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats ->
+        val statsFlow = when (currentDateType) {
+            DateType.WEEK -> {
+                viewModel.getWeeklyStats(currentDate.timeInMillis, 6)
+            }
+            DateType.MONTH -> {
+                viewModel.getYearMonthlyStats(currentDate.timeInMillis)
+            }
+            else -> return
+        }
+
+        statsFlow.observe(viewLifecycleOwner) { stats ->
             if (stats.isEmpty()) return@observe
             
             val entries = stats.mapIndexed { index, stat ->
@@ -181,58 +211,77 @@
             }
 
             val dataSet = BarDataSet(entries, "包裹数量")
-            dataSet.color = resources.getColor(R.color.purple_500)
-            dataSet.valueTextSize = 12f
+            dataSet.apply {
+                color = resources.getColor(R.color.purple_500)
+                valueTextSize = 12f
+                valueFormatter = object : ValueFormatter() {
+                    override fun getFormattedValue(value: Float): String {
+                        return value.toInt().toString()
+                    }
+                }
+            }
 
             val barData = BarData(dataSet)
             barChart.data = barData
             
-            // 设置X轴标签
+            // 修改X轴标签显示
             barChart.xAxis.apply {
                 valueFormatter = object : ValueFormatter() {
                     override fun getFormattedValue(value: Float): String {
                         val position = value.toInt()
                         if (position >= 0 && position < stats.size) {
-                            return "第${stats[position].date}周"
+                            return when(currentDateType) {
+                                DateType.WEEK -> {
+                                    val weekStat = stats[position]
+                                    val calendar = Calendar.getInstance()
+                                    calendar.timeInMillis = weekStat.weekStart!!
+                                    SimpleDateFormat("MM/dd", Locale.getDefault()).format(calendar.time)
+                                }
+                                DateType.MONTH -> {
+                                    // 显示月份标签(1-12月)
+                                    "${position + 1}月"
+                                }
+                                else -> ""
+                            }
                         }
                         return ""
                     }
                 }
+                position = XAxis.XAxisPosition.BOTTOM
+                setDrawGridLines(false)
                 labelCount = stats.size
                 granularity = 1f
                 labelRotationAngle = -45f
                 textSize = 10f
-                setDrawGridLines(false)
             }
             
-            // 调整图表显示
-            barChart.apply {
-                setVisibleXRangeMaximum(5f)
-                moveViewToX(0f)
-                
-                axisLeft.apply {
-                    axisMinimum = 0f
-                    granularity = 1f
-                    setDrawGridLines(true)
-                }
-                
-                barData.barWidth = 0.6f
-                description.isEnabled = false
-                legend.isEnabled = false
-                
-                invalidate()
+            // 高亮当前月份
+            if (currentDateType == DateType.MONTH) {
+                val currentMonth = currentDate.get(Calendar.MONTH)
+                dataSet.setColors(List(stats.size) { index ->
+                    if (index == currentMonth) resources.getColor(R.color.purple_500)
+                    else resources.getColor(R.color.purple_200)
+                })
+            } else if (currentDateType == DateType.WEEK) {
+                // 保持周视图的高亮逻辑
+                val highlightIndex = 3f
+                dataSet.setColors(List(stats.size) { index ->
+                    if (index == 3) resources.getColor(R.color.purple_500)
+                    else resources.getColor(R.color.purple_200)
+                })
             }
+            
+            barChart.invalidate()
         }
     }
     private fun setupPieChart() {
         pieChart.apply {
             description.isEnabled = false
-            setUsePercentValues(false) // 改为显示实际数量
+            setUsePercentValues(false)
             setDrawEntryLabels(false)
             
-            // 增大饼图尺寸
-            setExtraOffsets(20f, 10f, 80f, 10f)
-            minimumHeight = (resources.displayMetrics.density * 400).toInt() // 设置最小高度
+            // 调整饼图边距
+            setExtraOffsets(20f, 10f, 60f, 10f)
             
             // 配置图例
             legend.apply {
@@ -241,17 +290,24 @@
                 horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT
                 orientation = Legend.LegendOrientation.VERTICAL
                 setDrawInside(false)
-                xEntrySpace = 7f
-                yEntrySpace = 0f
+                xEntrySpace = 10f
+                yEntrySpace = 5f
                 yOffset = 0f
-                textSize = 12f // 增大图例文字大小
+                textSize = 14f
             }
+
+            // 设置中心空白
+            holeRadius = 45f
+            transparentCircleRadius = 50f
         }
 
         updatePieChartData()
     }
     private fun updatePieChartData() {
-        viewModel.getCourierStats(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats ->
+        viewModel.getCourierStats(
+            currentDate.timeInMillis,
+            currentDateType.name
+        ).observe(viewLifecycleOwner) { stats ->
             val entries = stats.map { stat ->
                 PieEntry(stat.count.toFloat(), "${stat.courierName}(${stat.count})")
             }
@@ -281,36 +337,132 @@
     }
 
     private fun loadPackages() {
-        // 这里应该从数据库或网络加载数据
-        // 根据当前选择的日期类型传入对应参数
-//        val packages = when (currentDateType) {
-//            DateType.DAY -> Core.code.getPackagesByDay(currentDate.timeInMillis)
-//            DateType.WEEK -> Core.code.getPackagesByWeek(currentDate.timeInMillis)
-//            DateType.MONTH -> Core.code.getPackagesByMonth(currentDate.timeInMillis)
-//            DateType.YEAR -> Core.code.getPackagesByYear(currentDate.timeInMillis)
-//        }
-        val packages =Core.code.getPackagesByDay(currentDate.timeInMillis)
-        packageAdapter.updatePackages(packages)
-        binding.textPackageCount.text = "${packageAdapter.itemCount}个"
-    }
-    private fun observePackages() {
         viewModel.getPackages(
             currentDate.timeInMillis,
             currentDateType.name
         ).observe(viewLifecycleOwner) { packages ->
-            when (currentDateType) {
-                DateType.WEEK -> {
-                    // 更新图表数据
-                    updateBarChartData()
-                    updatePieChartData()
-                }
-                else -> {
-                packageAdapter.updatePackages(packages)
-            }
-            }
+            packageAdapter.updatePackages(packages)
             binding.textPackageCount.text = "${packages.size}个"
         }
     }
+    private fun setupHeatmap() {
+        heatmapView.visibility = View.GONE
+    }
+
+    private fun updateHeatmapData() {
+        viewModel.getYearlyHeatmap(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats ->
+            if (stats.isEmpty()) return@observe
+
+            // 创建52周x7天的数据矩阵
+            val heatmapMatrix = Array(7) { IntArray(52) }
+            
+            // 填充数据
+            stats.forEach { stat ->
+                val week = stat.weekOfYear - 1 // 0-51
+                val dayOfWeek = stat.dayOfWeek - 1 // 0-6
+                if (week in 0..51 && dayOfWeek in 0..6) {
+                    heatmapMatrix[dayOfWeek][week] = stat.count
+                }
+            }
+
+            // 更新UI
+            binding.layoutWeekStats.heatmapYearly.apply {
+                // 清除现有的子视图
+                removeAllViews()
+                
+                // 创建网格布局
+                val gridLayout = GridLayout(context).apply {
+                    rowCount = 7
+                    columnCount = 52
+                }
+
+                // 添加日期标签
+                val dayLabels = arrayOf("周日", "周一", "周二", "周三", "周四", "周五", "周六")
+                for (i in 0..6) {
+                    val label = TextView(context).apply {
+                        text = dayLabels[i]
+                        textSize = 10f
+                        setPadding(0, 0, 8, 0)
+                    }
+                    gridLayout.addView(label)
+                }
+
+                // 添加热力图单元格
+                for (day in 0..6) {
+                    for (week in 0..51) {
+                        val count = heatmapMatrix[day][week]
+                        val cell = View(context).apply {
+                            layoutParams = ViewGroup.LayoutParams(
+                                resources.getDimensionPixelSize(R.dimen.heatmap_cell_size),
+                                resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
+                            )
+                            setBackgroundColor(getHeatmapColor(count))
+                            setPadding(1, 1, 1, 1)
+                        }
+                        gridLayout.addView(cell)
+                    }
+                }
+
+                addView(gridLayout)
+            }
+        }
+    }
+
+    private fun getHeatmapColor(count: Int): Int {
+        // 根据数量返回不同深浅的颜色
+        return when {
+            count == 0 -> Color.parseColor("#EBEDF0")
+            count <= 2 -> Color.parseColor("#9BE9A8")
+            count <= 4 -> Color.parseColor("#40C463")
+            count <= 6 -> Color.parseColor("#30A14E")
+            else -> Color.parseColor("#216E39")
+        }
+    }
+
+    private fun updateCharts() {
+        when (currentDateType) {
+            DateType.DAY -> {
+                // 日视图显示包裹列表,隐藏统计图表
+                binding.recyclerPackages.visibility = View.VISIBLE
+                binding.layoutWeekStats.root.visibility = View.GONE
+                binding.layoutYearStats.root.visibility = View.GONE
+            }
+            DateType.WEEK, DateType.MONTH -> {
+                // 周和月视图显示柱状图和饼图,隐藏包裹列表
+                binding.recyclerPackages.visibility = View.GONE
+                binding.layoutWeekStats.root.visibility = View.VISIBLE
+                binding.layoutYearStats.root.visibility = View.GONE
+                binding.layoutWeekStats.chartDailyPackages.visibility = View.VISIBLE
+                binding.layoutWeekStats.heatmapYearly.visibility = View.GONE
+                updateBarChartData()
+                updatePieChartData()
+            }
+            DateType.YEAR -> {
+                // 年视图显示热力图和饼图,隐藏包裹列表和柱状图
+                binding.recyclerPackages.visibility = View.GONE
+                binding.layoutWeekStats.root.visibility = View.VISIBLE
+                binding.layoutYearStats.root.visibility = View.VISIBLE
+                binding.layoutWeekStats.chartDailyPackages.visibility = View.GONE
+                binding.layoutWeekStats.heatmapYearly.visibility = View.VISIBLE
+                updateHeatmapData()
+                updatePieChartData()
+            }
+        }
+    }
+
+    private fun updateYearlyStats() {
+        viewModel.getYearlyStats(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats: List<DailyStat> ->
+            if (stats.isEmpty()) return@observe
+            
+            // 更新年度包裹总数
+            binding.layoutYearStats.textTotalPackages.text = "${stats.sumOf { it.count }}个"
+            
+            // 更新平均每天包裹数
+            val avgDaily = stats.sumOf { it.count }.toFloat() / 365
+            binding.layoutYearStats.textDailyAverage.text = String.format("%.2f", avgDaily)
+        }
+    }
+
     override fun onDestroyView() {
         super.onDestroyView()
         _binding = null
diff --git a/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt
index c2c0fc9..6d8b8fe 100644
--- a/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt
+++ b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt
@@ -21,9 +21,20 @@
     fun getPackages(date: Long, dateType: String) =
         repository.getPackages(date, dateType).asLiveData()
 
-    fun getCourierStats(date: Long) = repository.getCourierStats(date).asLiveData()
+    fun getCourierStats(date: Long, dateType: String) = 
+        repository.getCourierStats(date, dateType).asLiveData()
 
-    fun getDailyStats(date: Long) = repository.getDailyStats(date).asLiveData()
+    fun getYearlyHeatmap(date: Long) = repository.getYearlyHeatmap(date).asLiveData()
+
+    fun getWeeklyStats(date: Long, weekCount: Int = 6) = 
+        repository.getWeeklyStats(date, weekCount).asLiveData()
+    
+    fun getMonthlyStats(date: Long) = repository.getMonthlyStats(date).asLiveData()
+    
+    fun getYearlyStats(date: Long) = repository.getYearlyStats(date).asLiveData()
+
+    fun getYearMonthlyStats(date: Long) = 
+        repository.getYearMonthlyStats(date).asLiveData()
 
     fun insert(code: Code) = viewModelScope.launch {
         repository.insert(code)
diff --git a/app/src/main/res/drawable/ic_average.xml b/app/src/main/res/drawable/ic_average.xml
new file mode 100644
index 0000000..c523f42
--- /dev/null
+++ b/app/src/main/res/drawable/ic_average.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"/>
+</vector> 
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_package.xml b/app/src/main/res/drawable/ic_package.xml
new file mode 100644
index 0000000..6c7e5cb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_package.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M21,16.5c0,0.38 -0.21,0.71 -0.53,0.88l-7.9,4.44c-0.16,0.12 -0.36,0.18 -0.57,0.18s-0.41,-0.06 -0.57,-0.18l-7.9,-4.44A0.991,0.991 0,0 1,3 16.5L3,7.5c0,-0.38 0.21,-0.71 0.53,-0.88l7.9,-4.44c0.16,-0.12 0.36,-0.18 0.57,-0.18s0.41,0.06 0.57,0.18l7.9,4.44c0.32,0.17 0.53,0.5 0.53,0.88v9zM12,4.15L6.04,7.5 12,10.85l5.96,-3.35L12,4.15zM5,15.91l6,3.38v-6.71L5,9.21v6.7zM19,15.91v-6.7l-6,3.37v6.71l6,-3.38z"/>
+</vector> 
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml
index ddd092e..5b9b473 100644
--- a/app/src/main/res/layout/fragment_dashboard.xml
+++ b/app/src/main/res/layout/fragment_dashboard.xml
@@ -101,15 +101,22 @@
         </LinearLayout>
     </androidx.cardview.widget.CardView>
 
-    <!-- 包裹列表 -->
+    <!-- 年度统计布局 -->
+    <include
+        android:id="@+id/layout_year_stats"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:layout_constraintTop_toBottomOf="@id/card_package_stats"
+        layout="@layout/layout_year_stats" />
+
+    <!-- 包裹列表和统计图表 -->
     <androidx.core.widget.NestedScrollView
         android:layout_width="match_parent"
         android:layout_height="0dp"
         android:fillViewport="true"
-        android:clipToPadding="false"
-        app:layout_constraintTop_toBottomOf="@id/card_package_stats"
-        app:layout_constraintBottom_toBottomOf="parent"
-        android:layout_marginBottom="56dp">
+        app:layout_constraintTop_toBottomOf="@id/layout_year_stats"
+        app:layout_constraintBottom_toBottomOf="parent">
 
         <ViewFlipper
             android:id="@+id/view_flipper_stats"
diff --git a/app/src/main/res/layout/layout_week_stats.xml b/app/src/main/res/layout/layout_week_stats.xml
index 6e1c3e8..e0cb566 100644
--- a/app/src/main/res/layout/layout_week_stats.xml
+++ b/app/src/main/res/layout/layout_week_stats.xml
@@ -4,7 +4,7 @@
     android:layout_height="wrap_content"
     android:orientation="vertical">
 
-    <!-- 每日包裹数量柱状图 -->
+    <!-- 柱状图容器 -->
     <androidx.cardview.widget.CardView
         android:layout_width="match_parent"
         android:layout_height="280dp"
@@ -19,10 +19,10 @@
             android:padding="16dp" />
     </androidx.cardview.widget.CardView>
 
-    <!-- 快递公司占比饼图 -->
+    <!-- 饼图容器 -->
     <androidx.cardview.widget.CardView
         android:layout_width="match_parent"
-        android:layout_height="280dp"
+        android:layout_height="400dp"
         android:layout_marginHorizontal="16dp"
         android:layout_marginBottom="16dp">
 
@@ -32,4 +32,29 @@
             android:layout_height="match_parent"
             android:padding="16dp" />
     </androidx.cardview.widget.CardView>
+
+    <!-- 热力图容器 -->
+    <androidx.cardview.widget.CardView
+        android:id="@+id/heatmap_yearly"
+        android:layout_width="match_parent"
+        android:layout_height="280dp"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="16dp"
+        android:visibility="gone">
+
+        <HorizontalScrollView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:padding="16dp">
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:orientation="horizontal">
+                <!-- 热力图将在这里动态添加 -->
+            </LinearLayout>
+        </HorizontalScrollView>
+    </androidx.cardview.widget.CardView>
+
 </LinearLayout> 
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_year_stats.xml b/app/src/main/res/layout/layout_year_stats.xml
new file mode 100644
index 0000000..7f3c243
--- /dev/null
+++ b/app/src/main/res/layout/layout_year_stats.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:padding="16dp">
+
+    <!-- 年度包裹总数 -->
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:gravity="center">
+
+        <ImageView
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:src="@drawable/ic_package" />
+
+        <TextView
+            android:id="@+id/text_total_packages"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="24sp"
+            android:textStyle="bold"
+            android:text="0个" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="年度包裹总数" />
+    </LinearLayout>
+
+    <!-- 平均每天包裹数 -->
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:gravity="center">
+
+        <ImageView
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:src="@drawable/ic_average" />
+
+        <TextView
+            android:id="@+id/text_daily_average"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="24sp"
+            android:textStyle="bold"
+            android:text="0.00" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="平均每天包裹数" />
+    </LinearLayout>
+</LinearLayout> 
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index d4ac5d6..29be459 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -4,4 +4,5 @@
     <dimen name="activity_vertical_margin">16dp</dimen>
     <dimen name="list_item_spacing">16dp</dimen>
     <dimen name="list_item_spacing_half">8dp</dimen>
+    <dimen name="heatmap_cell_size">12dp</dimen>
 </resources>
\ No newline at end of file

--
Gitblit v1.9.3