zhujie
2025-04-03 db2e408f6c2eddc363ed76e0f5fd1e59a02aa175
app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
@@ -1,38 +1,553 @@
package com.example.firstapp.ui.dashboard
import com.example.firstapp.R
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
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.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.ValueFormatter
import java.util.*
import java.text.SimpleDateFormat
import android.graphics.Color
import android.widget.GridLayout
import com.example.firstapp.model.DailyStat
import com.github.mikephil.charting.components.YAxis
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
    }
    private val viewModel: DashboardViewModel by viewModels()
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val dashboardViewModel =
            ViewModelProvider(this).get(DashboardViewModel::class.java)
        _binding = FragmentDashboardBinding.inflate(inflater, container, false)
        val root: View = binding.root
        return binding.root
    }
        val textView: TextView = binding.textDashboard
        dashboardViewModel.text.observe(viewLifecycleOwner) {
            textView.text = it
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //渲染包裹列表
        setupRecyclerView()
        //初始化tab内容和数据
        setupTabLayout()
        //日期调整
        setupDatePicker()
        setupView(view)
        updateDateDisplay()
        loadPackages()
    }
    private fun setupRecyclerView() {
        binding.recyclerPackages.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = packageAdapter
        }
        return root
    }
    private fun setupTabLayout() {
        binding.tabDateRange.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab?) {
                currentDateType = when(tab?.position) {
                    0 -> DateType.DAY
                    1 -> DateType.WEEK
                    2 -> DateType.MONTH
                    3 -> DateType.YEAR
                    else -> DateType.DAY
                }
                updateDateDisplay()
                updateCharts()
                loadPackages()
            }
            override fun onTabUnselected(tab: TabLayout.Tab?) {}
            override fun onTabReselected(tab: TabLayout.Tab?) {}
        })
    }
    private fun setupDatePicker() {
        binding.btnPreviousDate.setOnClickListener {
            adjustDate(-1)
        }
        binding.btnNextDate.setOnClickListener {
            adjustDate(1)
        }
    }
    private fun adjustDate(amount: Int) {
        when (currentDateType) {
            DateType.DAY -> currentDate.add(Calendar.DAY_OF_MONTH, amount)
            DateType.WEEK -> currentDate.add(Calendar.WEEK_OF_YEAR, amount)
            DateType.MONTH -> currentDate.add(Calendar.MONTH, amount)
            DateType.YEAR -> currentDate.add(Calendar.YEAR, amount)
        }
        updateDateDisplay()
        updateCharts()
        loadPackages()
    }
    private fun updateDateDisplay() {
        val dateFormat = when (currentDateType) {
            DateType.DAY -> "yyyy年MM月dd日"
            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年"
        }
        if (currentDateType == DateType.WEEK) {
            binding.textCurrentDate.text = dateFormat
        } else {
            binding.textCurrentDate.text = SimpleDateFormat(dateFormat, Locale.getDefault())
                .format(currentDate.time)
        }
    }
    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 {
            description.isEnabled = false
            setDrawGridBackground(false)
            legend.isEnabled = false
            // 增大图表高度
            minimumHeight = (resources.displayMetrics.density * 300).toInt()
            // X轴设置
            xAxis.apply {
                position = XAxis.XAxisPosition.BOTTOM
                setDrawGridLines(false)
                granularity = 1f
                labelRotationAngle = 0f
                textSize = 10f  //标签字体
                setExtraLeftOffset(5f)  // 减少左侧留白
                setExtraBottomOffset(15f)
            }
            // Y轴设置
            axisLeft.apply {
                setDrawGridLines(true)
                axisMinimum = 0f
                granularity = 1f
                valueFormatter = object : ValueFormatter() {
                    override fun getFormattedValue(value: Float): String {
                        return if (value > 0) value.toInt().toString() else ""
                    }
                }
            }
            axisRight.isEnabled = false
            // 设置图表交互
            setTouchEnabled(true)
            isDragEnabled = true
            setScaleEnabled(true)
            // 设置边距
            setExtraOffsets(10f, 10f, 10f, 20f)
        }
        updateBarChartData()
    }
    private fun updateBarChartData() {
        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 ->
                val entry = BarEntry(index.toFloat(), stat.count.toFloat())
                if (stat.count == 0) {
                    entry.data = "hide_label"
                }
                entry
            }
            val dataSet = BarDataSet(entries, "包裹数量")
            dataSet.apply {
                color = resources.getColor(R.color.purple_500)
                valueTextSize = 12f
                valueFormatter = object : ValueFormatter() {
                    override fun getFormattedValue(value: Float): String {
                        return if (value > 0) value.toInt().toString() else ""
                    }
                }
            }
            val barData = BarData(dataSet)
            barChart.data = barData
            // 修改X轴标签显示
            barChart.xAxis.apply {
                valueFormatter = object : ValueFormatter() {
                    override fun getFormattedValue(value: Float): String {
                        val position = value.toInt()
                        if (position >= 0 && position < stats.size) {
                            return when(currentDateType) {
                                DateType.WEEK -> {
                                    val weekStat = stats[position]
                                    try {
                                        // 解析日期字符串
                                        val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
                                        val date = sdf.parse(weekStat.date)
                                        val calendar = Calendar.getInstance()
                                        calendar.time = date
                                        // 获取年份和周数
                                        val weekOfYear = calendar.get(Calendar.WEEK_OF_YEAR)
                                        val year = calendar.get(Calendar.YEAR)
                                        // 显示格式:第X周
//                                        "第${weekOfYear}周"
                                        "${weekOfYear}"
                                    } catch (e: Exception) {
                                        weekStat.date
                                    }
                                }
//                                DateType.MONTH -> "${stats[position].date}月"
                                DateType.MONTH -> {
                                    stats[position].date.replaceFirst("^0+".toRegex(), "")
                                }
                                else -> ""
                            }
                        }
                        return ""
                    }
                }
                labelCount = stats.size
                setAvoidFirstLastClipping(true)
                labelRotationAngle = 0f
                // 增加标签间距
//                setExtraBottomOffset(20f)
                textSize = 11f  // 稍微调小字体
            }
            // 高亮显示
            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 = 3 // 当前周在第4个位置
                dataSet.setColors(List(stats.size) { index ->
                    if (index == highlightIndex) resources.getColor(R.color.purple_500)
                    else resources.getColor(R.color.purple_200)
                })
            }
            // 刷新图表
            barChart.notifyDataSetChanged()
            barChart.invalidate()
        }
    }
    private fun setupPieChart() {
        pieChart.apply {
            // 隐藏图表描述(右下角默认文字)
            description.isEnabled = false
            // 显示实际值而非百分比
            setUsePercentValues(false)
            // 隐藏饼图区块上的标签(如数值或名称)
            setDrawEntryLabels(false)
            // 设置饼图与容器的边距,参数顺序:左、上、右、下
            // 右侧留出 80f 空间,可能为图例腾出位置
            setExtraOffsets(20f, 20f, 20f, 20f)
            // 配置图例
            legend.apply {
                isEnabled = true // 启用图例
                verticalAlignment = Legend.LegendVerticalAlignment.CENTER // 垂直居中
                horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT // 水平靠右对齐
                orientation = Legend.LegendOrientation.VERTICAL // 图例项垂直排列
                setDrawInside(false)  // 图例绘制在图表外部(而非覆盖在图上)
                xEntrySpace = 10f  // 图例项水平间距
                yEntrySpace = 5f  // 图例项垂直间距
                yOffset = 0f      // 图例整体 Y 轴无偏移
                textSize = 12f   // 图例文字大小
            }
            // 调整中心空白区域大小
            holeRadius = 20f  // 中间空心圆的半径(占饼图比例)
            transparentCircleRadius = 25f  // 透明圆圈的半径(可能用于边框效果)
        }
        updatePieChartData()
    }
    private fun updatePieChartData() {
        viewModel.getCourierStats(
            currentDate.timeInMillis,
            currentDateType.name
        ).observe(viewLifecycleOwner) { stats ->
            val entries = stats.map { stat ->
                PieEntry(stat.count.toFloat(), "${stat.courierName}(${stat.count})")
            }
            val dataSet = PieDataSet(entries, "快递公司分布")
            dataSet.colors = listOf(
                resources.getColor(R.color.purple_500),
                resources.getColor(R.color.teal_200),
                resources.getColor(R.color.purple_200),
                resources.getColor(R.color.teal_700)
            )
            dataSet.valueTextSize = 14f // 增大数值文字大小
            val pieData = PieData(dataSet)
            pieData.setValueFormatter(object : ValueFormatter() {
                override fun getFormattedValue(value: Float): String {
                    return value.toInt().toString()
                }
            })
            pieChart.data = pieData
            pieChart.invalidate()
        }
    }
    private fun getDayLabels(): Array<String> {
        return arrayOf("周一", "周二", "周三", "周四", "周五", "周六", "周日")
    }
    private fun loadPackages() {
        viewModel.getPackages(
            currentDate.timeInMillis,
            currentDateType.name
        ).observe(viewLifecycleOwner) { packages ->
            when (currentDateType) {
                DateType.DAY -> {
                    binding.textPackageCount.text = "${packages.size}个"
                }
                DateType.WEEK -> {
                    // 获取本周统计
                    viewModel.getCurrentWeekStats(currentDate.timeInMillis)
                        .observe(viewLifecycleOwner) { stats ->
                            val weekTotal = stats.sumOf { it.count }
                            binding.textPackageCount.text = "${weekTotal}个"
                        }
                }
                DateType.MONTH -> {
                    // 获取本月统计
                    viewModel.getMonthlyStats(currentDate.timeInMillis)
                        .observe(viewLifecycleOwner) { stats ->
                            val monthTotal = stats.sumOf { it.count }
                            binding.textPackageCount.text = "${monthTotal}个"
                        }
                }
                DateType.YEAR -> {
                    // 获取本年统计
                    viewModel.getCurrentYearStats(currentDate.timeInMillis)
                        .observe(viewLifecycleOwner) { stats ->
                            val yearTotal = stats.sumOf { it.count }
                            binding.textPackageCount.text = "${yearTotal}个"
                        }
                }
            }
            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 = 8 // 增加一行用于显示月份
                    columnCount = 53 // 增加一列用于显示星期标签
                }
                // 添加月份标签
                val months = arrayOf("1月", "2月", "3月", "4月", "5月", "6月",
                                   "7月", "8月", "9月", "10月", "11月", "12月")
                months.forEachIndexed { index, month ->
                    val label = TextView(context).apply {
                        text = month
                        textSize = 10f
                        setPadding(0, 0, 8, 4)
                        // 计算每个月份标签的位置
                        val weekPosition = (index * 4.3).toInt()
                        val params = GridLayout.LayoutParams()
                        params.columnSpec = GridLayout.spec(weekPosition + 1)
                        params.rowSpec = GridLayout.spec(0)
                        layoutParams = params
                    }
                    gridLayout.addView(label)
                }
                // 添加星期标签
                val dayLabels = arrayOf("周一", "周二", "周三", "周四", "周五", "周六", "周日")
                dayLabels.forEachIndexed { index, label ->
                    val textView = TextView(context).apply {
                        text = label
                        textSize = 10f
                        setPadding(4, 0, 8, 0)
                        val params = GridLayout.LayoutParams()
                        params.columnSpec = GridLayout.spec(0)
                        params.rowSpec = GridLayout.spec(index + 1)
                        layoutParams = params
                    }
                    gridLayout.addView(textView)
                }
                // 添加热力图单元格
                for (day in 0..6) {
                    for (week in 0..51) {
                        val count = heatmapMatrix[day][week]
                        val cell = View(context).apply {
                            val params = GridLayout.LayoutParams().apply {
                                width = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                                height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                                columnSpec = GridLayout.spec(week + 1)
                                rowSpec = GridLayout.spec(day + 1)
                                setMargins(1, 1, 1, 1)
                            }
                            layoutParams = params
                            setBackgroundColor(getHeatmapColor(count))
                        }
                        gridLayout.addView(cell)
                    }
                }
                addView(gridLayout)
            }
        }
    }
    private fun getHeatmapColor(count: Int): Int {
        return when {
            count == 0 -> Color.parseColor("#EBEDF0")
            count == 1 -> Color.parseColor("#9BE9A8")
            count == 2 -> Color.parseColor("#40C463")
            count <= 4 -> 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
                binding.cardPackageStats.visibility = View.VISIBLE
            }
            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.chartDailyPackages.parent as? View)?.visibility = View.VISIBLE
                binding.layoutWeekStats.heatmapYearly.visibility = View.GONE
                binding.cardPackageStats.visibility = View.VISIBLE
                updateBarChartData()
                updatePieChartData()
            }
            DateType.YEAR -> {
                // 年视图显示热力图和饼图,隐藏包裹列表和柱状图
                binding.recyclerPackages.visibility = View.GONE
                binding.layoutWeekStats.root.visibility = View.VISIBLE
                binding.layoutYearStats.root.visibility = View.VISIBLE
                (binding.layoutWeekStats.chartDailyPackages.parent as? View)?.visibility = View.GONE
                binding.layoutWeekStats.heatmapYearly.visibility = View.VISIBLE
                binding.cardPackageStats.visibility = View.GONE
                updateHeatmapData()
                updatePieChartData()
                updateYearlyStats()
            }
        }
    }
    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() {