11、我的 切换头像
点击切换头像没有显示允许存储权限的窗口,华为的手机目前有
49、首页 实时刷新
点击全部取件或其他分类后,回到上一层,内容没有刷新(5个分类)
52、数据统计 数据统计
1.周月年的柱状图统计逻辑需要修改为只统计快递类的数据
2.按年的图形统计,右下方加上图示说明
53、首页 首页登录
点击用户协议、隐私政策无反应
| | |
| | | <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <!-- 允许语言识别。 --> |
| | | <uses-permission android:name="android.permission.RECORD_AUDIO" /> |
| | | |
| | | <!-- Android 13 及以上 --> |
| | | <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> |
| | | |
| | | <application |
| | | android:name=".App" |
| | | android:allowBackup="true" |
| | |
| | | |
| | | binding.tvUserAgreement.setOnClickListener { |
| | | // 打开用户协议 |
| | | startContentActivity("用户协议", "服务使用协议") |
| | | } |
| | | |
| | | binding.tvPrivacyPolicy.setOnClickListener { |
| | | // 打开隐私政策 |
| | | startContentActivity("隐私协议", "隐私保护政策") |
| | | } |
| | | } |
| | | |
| | |
| | | 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 |
| | | LEFT JOIN code ON date(code.createTime) = dates.date and code.category='快递' |
| | | GROUP BY dates.date |
| | | ORDER BY dates.date |
| | | """) |
| | |
| | | ) |
| | | SELECT |
| | | strftime('%m', month_start) as date, |
| | | COUNT(code.id) as count, |
| | | COUNT(c.id) as count, |
| | | strftime('%Y-%m-%d', 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 |
| | | FROM months m |
| | | LEFT JOIN code c ON strftime('%Y-%m', c.createTime) = strftime('%Y-%m', m.month_start) and c.category='快递' |
| | | GROUP BY m.month_start |
| | | ORDER BY m.month_start ASC |
| | | """) |
| | | fun getYearMonthlyStats(date: Long): Flow<List<DailyStat>> |
| | | |
| | |
| | | COUNT(c.id) AS count, |
| | | '' AS weekStart |
| | | FROM dates d |
| | | LEFT JOIN code c ON date(c.createTime) = d.date_value |
| | | LEFT JOIN code c ON date(c.createTime) = d.date_value and c.category='快递' |
| | | GROUP BY d.date_value |
| | | ORDER BY d.date_value ASC |
| | | """) |
| | |
| | | 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 |
| | |
| | | |
| | | // 创建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) |
| | | } |
| | | } |
| | | |
| | | // 创建图例(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(gridLayout) |
| | | addView(legendLayout) |
| | | } |
| | | |
| | | addView(container) |
| | | } |
| | | } |
| | | } |
| | | |
| | | private fun getHeatmapColor(count: Int): Int { |
| | | return when { |
| | | count == 0 -> Color.parseColor("#EBECF1") // 最淡 |
| | | count < 5 -> Color.parseColor("#ACE7B1") |
| | | count < 10 -> Color.parseColor("#68C16F") |
| | | count < 15 -> Color.parseColor("#529F57") |
| | | else -> Color.parseColor("#356D40") // 最深 |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | private fun updateHeatmapData_bak() { |
| | | viewModel.getYearlyHeatmap(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats -> |
| | | if (stats.isEmpty()) return@observe |
| | | |
| | | // 创建52周x7天的数据矩阵 |
| | | val heatmapMatrix = Array(7) { IntArray(52) } |
| | | |
| | | // 填充数据 |
| | | stats.forEach { stat -> |
| | |
| | | } |
| | | } |
| | | |
| | | private fun getHeatmapColor(count: Int): Int { |
| | | private fun getHeatmapColor_bak(count: Int): Int { |
| | | return when { |
| | | count == 0 -> Color.parseColor("#EBEDF0") |
| | | count == 1 -> Color.parseColor("#9BE9A8") |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | private fun setupAdapters() { |
| | | binding.expressRecycler.apply { |
| | | layoutManager = LinearLayoutManager(context) |
| | |
| | | ) |
| | | |
| | | // 加载数据 |
| | | homeViewModel.loadExpressData() |
| | | homeViewModel.loadAllCategoryData() |
| | | // homeViewModel.loadExpressData() |
| | | // homeViewModel.loadFinanceData() |
| | | // homeViewModel.loadIncomeData() |
| | | // homeViewModel.loadFlightData() |
| | | // homeViewModel.loadTrainData() |
| | | |
| | | // 检查未读提醒数量 |
| | | homeViewModel.checkUnreadReminders() |
| | | |
| | | } |
| | | |
| | | override fun onPause() { |
| | |
| | | loadDataByType("火车票") |
| | | } |
| | | |
| | | |
| | | fun loadAllCategoryData() { |
| | | getFullCategories().forEach { category -> |
| | | loadDataByType(category.name) |
| | | } |
| | | } |
| | | |
| | | |
| | | fun getFullCategories(): List<CategoryConfig> { |
| | | return listOf( |
| | | CategoryConfig(1, "快递", 0, true), |
| | | CategoryConfig(2, "还款", 1, true), |
| | | CategoryConfig(3, "收入", 2, true), |
| | | CategoryConfig(4, "航班", 3, true), |
| | | CategoryConfig(5, "火车票", 4, true) |
| | | ) |
| | | } |
| | | |
| | | private fun loadCategories() { |
| | | viewModelScope.launch { |
| | | try { |
| | | // 默认完整分类列表 |
| | | val fullCategories = listOf( |
| | | CategoryConfig(1, "快递", 0, true), |
| | | CategoryConfig(2, "还款", 1, true), |
| | | CategoryConfig(3, "收入", 2, true), |
| | | CategoryConfig(4, "航班", 3, true), |
| | | CategoryConfig(5, "火车票", 4, true) |
| | | ) |
| | | // val fullCategories = listOf( |
| | | // CategoryConfig(1, "快递", 0, true), |
| | | // CategoryConfig(2, "还款", 1, true), |
| | | // CategoryConfig(3, "收入", 2, true), |
| | | // CategoryConfig(4, "航班", 3, true), |
| | | // CategoryConfig(5, "火车票", 4, true) |
| | | // ) |
| | | |
| | | val fullCategories=getFullCategories() |
| | | |
| | | // 基础分类(非会员可见) |
| | | val basicCategories = listOf( |
| | |
| | | private var selectedImageUri: Uri? = null |
| | | private var loadingDialog: AlertDialog? = null |
| | | |
| | | private val pickImage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> |
| | | if (result.resultCode == Activity.RESULT_OK) { |
| | | result.data?.data?.let { uri -> |
| | | selectedImageUri = uri |
| | | Glide.with(this) |
| | | .load(uri) |
| | | .circleCrop() |
| | | .into(binding.ivAvatar) |
| | | } |
| | | // private val pickImage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> |
| | | // if (result.resultCode == Activity.RESULT_OK) { |
| | | // result.data?.data?.let { uri -> |
| | | // selectedImageUri = uri |
| | | // Glide.with(this) |
| | | // .load(uri) |
| | | // .circleCrop() |
| | | // .into(binding.ivAvatar) |
| | | // } |
| | | // } |
| | | // } |
| | | |
| | | private val pickImageLauncher = registerForActivityResult( |
| | | ActivityResultContracts.GetContent() |
| | | ) { uri: Uri? -> |
| | | uri?.let { |
| | | selectedImageUri = uri |
| | | // 这里直接处理选中的头像 |
| | | Glide.with(this) |
| | | .load(it) |
| | | .into(binding.ivAvatar) // 替换成你的 ImageView id |
| | | |
| | | // 如果需要上传可以用 contentResolver.openInputStream(uri) |
| | | } |
| | | } |
| | | |
| | | |
| | | // 👇 就放在这里 |
| | | private val permissionLauncher = registerForActivityResult( |
| | | ActivityResultContracts.RequestPermission() |
| | | ) { isGranted: Boolean -> |
| | | if (isGranted) { |
| | | openGallery() |
| | | } else { |
| | | Toast.makeText(this, "需要权限才能选择头像", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | binding.ivAvatar.setOnClickListener { |
| | | checkAndRequestPermission() |
| | | // checkAndRequestPermission() |
| | | checkStoragePermission() |
| | | } |
| | | |
| | | binding.btnSaveBottom.setOnClickListener { |
| | | saveAndFinish() |
| | | } |
| | | } |
| | | |
| | | fun checkStoragePermission() { |
| | | val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
| | | Manifest.permission.READ_MEDIA_IMAGES |
| | | } else { |
| | | Manifest.permission.READ_EXTERNAL_STORAGE |
| | | } |
| | | |
| | | if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { |
| | | permissionLauncher.launch(permission) |
| | | } else { |
| | | openGallery() |
| | | } |
| | | } |
| | | |
| | | |
| | | private fun saveAndFinish() { |
| | | lifecycleScope.launch { |
| | |
| | | } |
| | | } |
| | | |
| | | private fun openGallery() { |
| | | private fun openGallery_bak() { |
| | | val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) |
| | | pickImage.launch(intent) |
| | | // pickImage.launch(intent) |
| | | } |
| | | |
| | | private fun openGallery() { |
| | | pickImageLauncher.launch("image/*") |
| | | // val intent = Intent(Intent.ACTION_PICK) |
| | | // intent.type = "image/*" |
| | | // startActivityForResult(intent, 1001) |
| | | } |
| | | |
| | | private fun showLoading() { |
| | |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginHorizontal="16dp" |
| | | android:layout_marginBottom="16dp"> |
| | | android:layout_marginBottom="16dp" |
| | | android:padding="16dp" |
| | | > |
| | | |
| | | <HorizontalScrollView |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:padding="16dp"> |
| | | <!-- <HorizontalScrollView--> |
| | | <!-- android:layout_width="match_parent"--> |
| | | <!-- android:layout_height="wrap_content"--> |
| | | <!-- android:padding="16dp">--> |
| | | |
| | | <LinearLayout |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="horizontal"> |
| | | <!-- 热力图将在这里动态添加 --> |
| | | </LinearLayout> |
| | | </HorizontalScrollView> |
| | | <!-- <LinearLayout--> |
| | | <!-- android:layout_width="wrap_content"--> |
| | | <!-- android:layout_height="wrap_content"--> |
| | | <!-- android:orientation="horizontal">--> |
| | | <!-- <!– 热力图将在这里动态添加 –>--> |
| | | <!-- </LinearLayout>--> |
| | | <!-- </HorizontalScrollView>--> |
| | | </androidx.cardview.widget.CardView> |
| | | |
| | | |