zhujie
8 天以前 acb5be78c07b8499d0a38515b05a4982207c1c9a
Merge branch 'master' of http://47.96.225.205:8888/r/FirstApp2
已修改7个文件
已添加1个文件
802 ■■■■■ 文件已修改
app/src/main/assets/calendar-heatmap.html 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/example/firstapp/MainActivity.kt 155 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/example/firstapp/activity/SettingActivity.kt 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt 433 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_phone_login.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_setting.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/layout_week_stats.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/dimens.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/assets/calendar-heatmap.html
对比新文件
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,height=device-height">
    <title>水平日历色块图</title>
    <style>::-webkit-scrollbar{display:none;}html,body{overflow:hidden;height:100%;margin:0;}</style>
</head>
<body>
<div id="mountNode"></div>
<script>/*Fixing iframe window.innerHeight 0 issue in Safari*/document.body.clientHeight;</script>
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g2-3.5.1/dist/g2.min.js"></script>
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.data-set-0.10.1/dist/data-set.min.js"></script>
<script src="https://gw.alipayobjects.com/os/antv/assets/lib/jquery-3.2.1.min.js"></script>
<script>
    var Shape = G2.Shape;
    var Util = G2.Util;
    Shape.registerShape('polygon', 'boundary-polygon', {
      draw: function draw(cfg, container) {
        if (!Util.isEmpty(cfg.points)) {
          var attrs = {
            stroke: '#fff',
            lineWidth: 1,
            fill: cfg.color,
            fillOpacity: cfg.opacity
          };
          var points = cfg.points;
          var path = [['M', points[0].x, points[0].y], ['L', points[1].x, points[1].y], ['L', points[2].x, points[2].y], ['L', points[3].x, points[3].y], ['Z']];
          attrs.path = this.parsePath(path);
          var polygon = container.addShape('path', {
            attrs: attrs
          });
          if (cfg.origin._origin.lastWeek) {
            var linePath = [['M', points[2].x, points[2].y], ['L', points[3].x, points[3].y]];
            // 最后一周的多边形添加右侧边框
            container.addShape('path', {
              zIndex: 1,
              attrs: {
                path: this.parsePath(linePath),
                lineWidth: 1,
                stroke: '#404040'
              }
            });
            if (cfg.origin._origin.lastDay) {
              container.addShape('path', {
                zIndex: 1,
                attrs: {
                  path: this.parsePath([['M', points[1].x, points[1].y], ['L', points[2].x, points[2].y]]),
                  lineWidth: 1,
                  stroke: '#404040'
                }
              });
            }
          }
          container.sort();
          return polygon;
        }
      }
    });
    $.getJSON('https://gw.alipayobjects.com/os/antvdemo/assets/data/github-commit.json', function(data) {
      var chart = new G2.Chart({
        container: 'mountNode',
        forceFit: true,
        height: window.innerHeight,
        padding: [window.innerHeight / 3, 20, window.innerHeight / 3, 80]
      });
      chart.source(data, {
        day: {
          type: 'cat',
          values: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
        },
        week: {
          type: 'cat'
        },
        commits: {
          sync: true
        }
      });
      chart.axis('week', {
        position: 'top',
        tickLine: null,
        line: null,
        label: {
          offset: 12,
          textStyle: {
            fontSize: 12,
            fill: '#666',
            textBaseline: 'top'
          },
          formatter: function formatter(val) {
            if (val === '2') {
              return 'MAY';
            } else if (val === '6') {
              return 'JUN';
            } else if (val === '10') {
              return 'JUL';
            } else if (val === '15') {
              return 'AUG';
            } else if (val === '19') {
              return 'SEP';
            } else if (val === '24') {
              return 'OCT';
            }
            return '';
          }
        }
      });
      chart.axis('day', {
        grid: null
      });
      chart.legend(false);
      chart.tooltip({
        title: 'date'
      });
      chart.coord().reflect('y');
      chart.polygon().position('week*day*date').color('commits', '#BAE7FF-#1890FF-#0050B3').shape('boundary-polygon');
      chart.render();
    });
</script>
</body>
</html>
app/src/main/java/com/example/firstapp/MainActivity.kt
@@ -14,6 +14,7 @@
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
@@ -31,6 +32,7 @@
import java.util.Calendar
import java.util.Date
import java.util.Locale
import android.app.AlertDialog
class MainActivity : AppCompatActivity() {
    // 安全防护关键词数组
@@ -40,23 +42,41 @@
    private var smsReceiver: SmsReceiver? = null
    private val multiplePermissionRequest =
    // 短信权限请求
    private val smsPermissionRequest =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
            when {
                permissions.getOrDefault(
                    Manifest.permission.RECEIVE_SMS, false
                ) && permissions.getOrDefault(Manifest.permission.READ_SMS, false) -> {
                    // 两个权限都获得授权
                    Toast.makeText(this, "短信权限已授予", Toast.LENGTH_SHORT).show()
                    registerSmsReceiver()
                    // 在Android O及以上版本同步最近短信
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        syncRecentSms()
                    }
                }
                else -> {
                    // 有权限被拒绝
                    Toast.makeText(
                        this, "需要短信读取和接收权限才能正常使用功能", Toast.LENGTH_SHORT
                    ).show()
                    showSmsPermissionExplanationDialog()
                }
            }
        }
    // 通知权限请求
    private val notificationPermissionRequest =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                // 权限已授予
                Toast.makeText(this, "通知权限已授予,您将能收到重要提醒", Toast.LENGTH_SHORT).show()
            } else {
                // 权限被拒绝
                Toast.makeText(this, "通知权限被拒绝,应用将无法发送提醒通知", Toast.LENGTH_SHORT).show()
            }
            // 无论通知权限是否授予,都继续请求短信权限
            requestSmsPermissions()
        }
    private val syncLock = Object()
@@ -76,39 +96,122 @@
        // 重置提醒计划并检查是否有错过的提醒
        resetReminders()
        // 开始权限请求流程
        startPermissionsFlow()
    }
        // 检查权限
        if (ContextCompat.checkSelfPermission(
                this, Manifest.permission.RECEIVE_SMS
            ) != android.content.pm.PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
                this, Manifest.permission.READ_SMS
            ) != android.content.pm.PackageManager.PERMISSION_GRANTED
        ) {
            // 同时请求两个权限
            multiplePermissionRequest.launch(
    override fun onResume() {
        super.onResume()
        // 每次恢复活动时检查短信权限
        checkAndHandleSmsPermissions(showDialog = false)
    }
    // 权限请求主流程
    private fun startPermissionsFlow() {
        // 先请求通知权限,再请求短信权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // Android 13+ 需要通知权限
            if (ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.POST_NOTIFICATIONS
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                notificationPermissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
            } else {
                // 已有通知权限,直接请求短信权限
                requestSmsPermissions()
            }
        } else {
            // 低版本Android无需请求通知权限,直接请求短信权限
            requestSmsPermissions()
        }
    }
    // 请求短信权限
    private fun requestSmsPermissions() {
        if (!hasSmsPermissions()) {
            // 没有短信权限,请求权限
            smsPermissionRequest.launch(
                arrayOf(
                    Manifest.permission.RECEIVE_SMS, Manifest.permission.READ_SMS
                )
            )
        } else {
            // 权限已经授予,继续执行相关操作
            // 已有短信权限,直接初始化短信处理
            registerSmsReceiver()
            syncRecentSms()
            // 在Android O及以上版本同步最近短信
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                syncRecentSms()
            }
        }
    }
    // 检查并处理短信权限(可选是否显示对话框)
    private fun checkAndHandleSmsPermissions(showDialog: Boolean = true) {
        if (!hasSmsPermissions()) {
            if (showDialog) {
                showSmsPermissionExplanationDialog()
            }
        } else {
            // 已有短信权限,确保接收器已注册
            if (smsReceiver == null) {
                registerSmsReceiver()
            }
        }
    }
    // 检查是否有短信权限
    private fun hasSmsPermissions(): Boolean {
        return ContextCompat.checkSelfPermission(
            this, Manifest.permission.RECEIVE_SMS
        ) == PackageManager.PERMISSION_GRANTED &&
        ContextCompat.checkSelfPermission(
            this, Manifest.permission.READ_SMS
        ) == PackageManager.PERMISSION_GRANTED
    }
    // 显示短信权限解释对话框
    private fun showSmsPermissionExplanationDialog() {
        AlertDialog.Builder(this)
            .setTitle("需要短信权限")
            .setMessage("应用需要短信权限来自动处理和分类您的短信信息,包括快递、付款等内容。没有此权限,应用核心功能将无法正常工作。")
            .setPositiveButton("授予权限") { _, _ ->
                requestSmsPermissions()
            }
            .setNegativeButton("暂时不要") { dialog, _ ->
                dialog.dismiss()
                Toast.makeText(this, "您可以稍后在设置中开启短信权限", Toast.LENGTH_LONG).show()
            }
            .setCancelable(false)
            .show()
    }
    // 提供给外部调用的请求通知权限方法
    fun requestNotificationPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // 判断是否已经有通知权限
            if (ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.POST_NOTIFICATIONS
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                notificationPermissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
            } else {
                Toast.makeText(this, "已经拥有通知权限", Toast.LENGTH_SHORT).show()
            }
        } else {
            // Android 13以下的版本不需要请求权限
            Toast.makeText(this, "当前系统版本无需单独请求通知权限", Toast.LENGTH_SHORT).show()
        }
    }
    private fun registerSmsReceiver() {
//        应用启动时执行 registerSmsReceiver()
//        创建 SmsReceiver 实例
//        注册广播接收器,开始监听短信
//        等待新短信到达
//        新短信到达时,系统发送广播
//        SmsReceiver 的 onReceive 方法被调用
//        处理短信内容
//        发送数据更新广播
//        MainActivity 接收到更新广播
//        更新 UI
        Log.d("SMS_DEBUG", "MainActivity收到数据更新广播")
        // 确保不重复注册
        if (smsReceiver != null) return
        Log.d("SMS_DEBUG", "MainActivity注册短信接收器")
        smsReceiver = SmsReceiver()
        val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)
        registerReceiver(smsReceiver, filter)
app/src/main/java/com/example/firstapp/activity/SettingActivity.kt
@@ -10,6 +10,12 @@
import android.widget.Button
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import android.os.Build
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.example.firstapp.databinding.ActivitySettingBinding
import com.example.firstapp.R
@@ -30,6 +36,26 @@
        ViewModelProvider(this).get(HomeViewModel::class.java)
    }
    private var isFullscreen: Boolean = false
    // 添加通知权限请求
    private val notificationPermissionRequest =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                // 权限已授予,可以发送通知
                AlertDialog.Builder(this)
                    .setTitle("通知权限")
                    .setMessage("通知权限已开启,您将能收到重要提醒")
                    .setPositiveButton("确定", null)
                    .show()
            } else {
                // 权限被拒绝
                AlertDialog.Builder(this)
                    .setTitle("通知权限")
                    .setMessage("通知权限被拒绝,应用将无法发送提醒通知。您可以在系统设置中手动开启权限。")
                    .setPositiveButton("确定", null)
                    .show()
            }
        }
    @SuppressLint("ClickableViewAccessibility")
    override fun onCreate(savedInstanceState: Bundle?) {
@@ -65,6 +91,8 @@
//        })
        // 通知权限
        setupNotificationPermission()
        // 退出登录
        logout()
        // 账号注销
@@ -76,6 +104,35 @@
    }
    private fun setupNotificationPermission() {
        binding.notificationPermission.setOnClickListener {
            // 请求通知权限(在Android 13及以上需要)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                if (ContextCompat.checkSelfPermission(
                        this,
                        Manifest.permission.POST_NOTIFICATIONS
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    notificationPermissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
                } else {
                    // 已经有权限
                    AlertDialog.Builder(this)
                        .setTitle("通知权限")
                        .setMessage("您已经开启了通知权限")
                        .setPositiveButton("确定", null)
                        .show()
                }
            } else {
                // Android 13以下版本不需要请求权限
                AlertDialog.Builder(this)
                    .setTitle("通知权限")
                    .setMessage("当前系统版本无需单独请求通知权限")
                    .setPositiveButton("确定", null)
                    .show()
            }
        }
    }
    private fun aboutCompany(){
        binding.aboutCompany.setOnClickListener {
app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
@@ -1,37 +1,40 @@
package com.example.firstapp.ui.dashboard
import com.example.firstapp.R
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.widget.GridLayout
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.firstapp.databinding.FragmentDashboardBinding
import com.google.android.material.tabs.TabLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.firstapp.R
import com.example.firstapp.adapter.PackageAdapter
import com.example.firstapp.database.response.UserInfo
import com.example.firstapp.database.service.RetrofitClient
import com.example.firstapp.databinding.FragmentDashboardBinding
import com.example.firstapp.model.DailyStat
import com.example.firstapp.utils.PreferencesManager
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.view.Gravity
import android.widget.GridLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.lifecycle.lifecycleScope
import com.example.firstapp.database.response.UserInfo
import com.example.firstapp.database.service.RetrofitClient
import com.example.firstapp.model.DailyStat
import com.example.firstapp.utils.PreferencesManager
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
class DashboardFragment : Fragment() {
@@ -57,6 +60,7 @@
    }
    private val viewModel: DashboardViewModel by viewModels()
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
@@ -74,6 +78,14 @@
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
//        val webView: WebView = binding.layoutWeekStats.webView
//        val webSettings = webView.settings
//        webSettings.javaScriptEnabled = true // 启用 JavaScript
//
//
//        // 加载本地的 HTML 文件
//        webView.loadUrl("file:///android_asset/calendar-heatmap.html")
        //渲染包裹列表
        setupRecyclerView()
@@ -763,7 +775,242 @@
        heatmapView.visibility = View.GONE
    }
    // 用于创建图例
    private fun createLegend(): LinearLayout {
        val legendLayout = LinearLayout(context).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.END or Gravity.CENTER_VERTICAL
            setPadding(8, 16, 8, 0)
        }
        val labelLow = TextView(context).apply {
            text = "少"
            textSize = 10f
            setPadding(0, 0, 8, 0)
        }
        legendLayout.addView(labelLow)
        val legendLevels = listOf(0, 5, 10, 15, 20)
        legendLevels.forEach { level ->
            val colorBox = View(context).apply {
                setBackgroundColor(getHeatmapColor(level))
                val size = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                layoutParams = LinearLayout.LayoutParams(size, size).apply {
                    marginEnd = 4
                }
            }
            val label = TextView(context).apply {
                textSize = 10f
                setPadding(0, 0, 8, 0)
            }
            legendLayout.addView(colorBox)
            legendLayout.addView(label)
        }
        val labelHigh = TextView(context).apply {
            text = "多"
            textSize = 10f
            setPadding(8, 0, 0, 0)
        }
        legendLayout.addView(labelHigh)
        return legendLayout
    }
    private fun updateHeatmapData() {
        viewModel.getYearlyHeatmap(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats ->
            if (stats.isEmpty()) return@observe
            val heatmapMatrix = Array(7) { IntArray(52) }
            stats.forEach { stat ->
                val week = stat.weekOfYear - 1
                val dayOfWeek = stat.dayOfWeek - 1
                if (week in 0..51 && dayOfWeek in 0..6) {
                    heatmapMatrix[dayOfWeek][week] = stat.count
                }
            }
            binding.layoutWeekStats.heatmapYearly.apply {
                removeAllViews()
                // 外层父布局:水平 LinearLayout,左固定周标签,右为横向滚动热力图
                val outerLayout = LinearLayout(context).apply {
                    orientation = LinearLayout.HORIZONTAL
                }
                // 左侧星期标签列
                val dayLabelLayout = LinearLayout(context).apply {
                    orientation = LinearLayout.VERTICAL
                    val dayLabels = arrayOf("周一", "周二", "周三", "周四", "周五", "周六", "周日")
                    // 顶部空白占位
                    addView(TextView(context).apply {
                        text = ""
                        textSize = 20f
                        height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                    })
                    dayLabels.forEach { label ->
                        val textView = TextView(context).apply {
                            text = label
                            textSize = 10f
                            height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                            setPadding(4, 0, 4, 0)
                        }
                        addView(textView)
                    }
                }
                // 右侧滚动部分
                val scrollView = HorizontalScrollView(context).apply {
                    isHorizontalScrollBarEnabled = false
                    layoutParams = LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                }
                val scrollContentLayout = LinearLayout(context).apply {
                    orientation = LinearLayout.VERTICAL
                }
                val gridLayout = GridLayout(context).apply {
                    rowCount = 8
                    columnCount = 52
                }
                // 添加月份标签(估算)
                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(10, 0, 8, 4)
                        val weekPosition = (index * 4.3).toInt()
                        layoutParams = GridLayout.LayoutParams().apply {
                            columnSpec = GridLayout.spec(weekPosition)
                            rowSpec = GridLayout.spec(0)
                        }
                    }
                    gridLayout.addView(label)
                }*/
                months.forEachIndexed { index, month ->
                    val label = TextView(context).apply {
                        text = month
                        textSize = 10f
                        setPadding(10, 0, 8, 4)
                        val weekPosition = (index * 4.3).toInt()
                        val span = 4  // 控制跨列范围
                        layoutParams = GridLayout.LayoutParams().apply {
                            columnSpec = GridLayout.spec(weekPosition, span, GridLayout.CENTER)
                            rowSpec = GridLayout.spec(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 = GridLayout.LayoutParams().apply {
                                width = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                                height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                                columnSpec = GridLayout.spec(week)
                                rowSpec = GridLayout.spec(day + 1)
                                setMargins(1, 1, 1, 1)
                            }
                            setBackgroundColor(getHeatmapColor(count))
                        }
                        gridLayout.addView(cell)
                    }
                }
                scrollContentLayout.addView(gridLayout)
                scrollView.addView(scrollContentLayout)
                // 添加两个主要部分到外层布局
                outerLayout.addView(dayLabelLayout)
                outerLayout.addView(scrollView)
                // 图例
                val legendLayout = LinearLayout(context).apply {
                    orientation = LinearLayout.HORIZONTAL
                    gravity = Gravity.END or Gravity.CENTER_VERTICAL
                    setPadding(8, 16, 8, 0)
                    layoutParams = LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                }
                val labelLow = TextView(context).apply {
                    text = "少"
                    textSize = 10f
                    setPadding(0, 0, 8, 0)
                }
                legendLayout.addView(labelLow)
                val legendLevels = listOf(0, 5, 10, 15, 20)
                legendLevels.forEach { level ->
                    val colorBox = View(context).apply {
                        setBackgroundColor(getHeatmapColor(level))
                        val size = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                        layoutParams = LinearLayout.LayoutParams(size, size).apply {
                            marginEnd = 4
                        }
                    }
                    val label = TextView(context).apply {
                        textSize = 10f
                        setPadding(0, 0, 8, 0)
                    }
                    legendLayout.addView(colorBox)
                    legendLayout.addView(label)
                }
                val labelHigh = TextView(context).apply {
                    text = "多"
                    textSize = 10f
                    setPadding(8, 0, 0, 0)
                }
                legendLayout.addView(labelHigh)
                // 总容器垂直布局:热力图 + 图例
               val container = LinearLayout(context).apply {
                    orientation = LinearLayout.VERTICAL
                    addView(outerLayout)
                    addView(legendLayout)
                }
//                addView(container)
                // 设置外部 margin
                val params = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT
                ).apply {
                    setMargins(16, 16, 16, 16)  // 左、上、右、下的 margin,单位是像素
                }
                // 添加到父布局并应用 margin
                addView(container, params)
            }
        }
    }
    private fun updateHeatmapData_a() {
        viewModel.getYearlyHeatmap(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats ->
            if (stats.isEmpty()) return@observe
@@ -789,8 +1036,10 @@
                }
                // 添加月份标签
                val months = arrayOf("1月", "2月", "3月", "4月", "5月", "6月",
                    "7月", "8月", "9月", "10月", "11月", "12月")
                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
@@ -898,6 +1147,154 @@
        }
    }
    private fun updateHeatmapData_aaa() {
        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 // 1行月份 + 7行星期
                    columnCount = 53 // 1列星期 + 52列周数
                }
                // 添加月份标签
                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() // 估算月份起始位置
                        layoutParams = GridLayout.LayoutParams().apply {
                            columnSpec = GridLayout.spec(weekPosition + 1)
                            rowSpec = GridLayout.spec(0)
                        }
                    }
                    gridLayout.addView(label)
                }
                // 添加星期标签
                val dayLabels = arrayOf("周一", "周二", "周三", "周四", "周五", "周六", "周日")
                dayLabels.forEachIndexed { index, label ->
                    val textView = TextView(context).apply {
                        text = label
                        textSize = 10f
                        setPadding(4, 0, 8, 0)
                        layoutParams = GridLayout.LayoutParams().apply {
                            columnSpec = GridLayout.spec(0)
                            rowSpec = GridLayout.spec(index + 1)
                        }
                    }
                    gridLayout.addView(textView)
                }
                // 添加热力图单元格
                for (day in 0..6) {
                    for (week in 0..51) {
                        val count = heatmapMatrix[day][week]
                        val cell = View(context).apply {
                            layoutParams = 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)
                            }
                            setBackgroundColor(getHeatmapColor(count))
                        }
                        gridLayout.addView(cell)
                    }
                }
                // ✅ 把 gridLayout 包裹进 HorizontalScrollView
                val scrollView = HorizontalScrollView(context).apply {
                    layoutParams = LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                    isHorizontalScrollBarEnabled = true
                }
                scrollView.addView(gridLayout)
                // 创建图例(Legend)
                val legendLayout = LinearLayout(context).apply {
                    orientation = LinearLayout.HORIZONTAL
                    gravity = Gravity.END or Gravity.CENTER_VERTICAL // 靠右对齐
                    setPadding(8, 16, 8, 0)
                    layoutParams = LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                }
                // 左侧“少”标签
                val labelLow = TextView(context).apply {
                    text = "少"
                    textSize = 10f
                    setPadding(0, 0, 8, 0)
                }
                legendLayout.addView(labelLow)
                // 渐变色块 + 数值
                val legendLevels = listOf(0, 5, 10, 15, 20)
                legendLevels.forEach { level ->
                    val colorBox = View(context).apply {
                        setBackgroundColor(getHeatmapColor(level))
                        val size = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size)
                        layoutParams = LinearLayout.LayoutParams(size, size).apply {
                            marginEnd = 4
                        }
                    }
                    val label = TextView(context).apply {
//                        text = "$level"
                        textSize = 10f
                        setPadding(0, 0, 8, 0)
                    }
                    legendLayout.addView(colorBox)
                    legendLayout.addView(label)
                }
                // 右侧“多”标签
                val labelHigh = TextView(context).apply {
                    text = "多"
                    textSize = 10f
                    setPadding(8, 0, 0, 0)
                }
                legendLayout.addView(labelHigh)
                // 垂直组合 gridLayout + legendLayout
                val container = LinearLayout(context).apply {
                    orientation = LinearLayout.VERTICAL
                    addView(scrollView)
                    addView(legendLayout)
                }
                addView(container)
            }
        }
    }
    private fun getHeatmapColor(count: Int): Int {
        return when {
            count == 0 -> Color.parseColor("#EBECF1") // 最淡
app/src/main/res/layout/activity_phone_login.xml
@@ -66,7 +66,7 @@
                android:layout_weight="1"
                android:background="@null"
                android:hint="请输入验证码"
                android:text="888888"
                android:text=""
                android:inputType="number"
                android:maxLength="6"
                android:textSize="16sp"
app/src/main/res/layout/activity_setting.xml
@@ -35,7 +35,7 @@
        </androidx.appcompat.widget.Toolbar>
        <LinearLayout
            android:id="@+id/about_company"
            android:id="@+id/notification_permission"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center"
@@ -48,6 +48,24 @@
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="5dp"
                android:text="开启通知权限"
                android:textColor="#000000" />
        </LinearLayout>
        <LinearLayout
            android:id="@+id/about_company"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center"
            android:orientation="vertical"
            android:background="#F9F9F9"
            android:layout_marginTop="1dp"
            >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="5dp"
                android:text="关于我们"
                android:textColor="#000000" />
        </LinearLayout>
app/src/main/res/layout/layout_week_stats.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
@@ -62,6 +63,14 @@
    </androidx.cardview.widget.CardView>
<!--    <WebView-->
<!--        android:id="@+id/webView"-->
<!--        android:layout_width="match_parent"-->
<!--        android:layout_height="300dp"-->
<!--        tools:ignore="WebViewLayout" />-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
app/src/main/res/values/dimens.xml
@@ -4,7 +4,7 @@
    <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>
    <dimen name="heatmap_cell_size">25dp</dimen>
    <dimen name="fab_margin">16dp</dimen>
    
    <!-- 徽章尺寸 -->