主要功能: 数字、日期、时间选择、底部导航 数据存储(程序重新打开,展现程序关闭时的数据) sqlite数据库使用 图表 sqlite导出数据xls,导入xls到sqlite数据库 package com.example.mynumset import android.app.Activity import android.content.Context import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.mynumset.ui.theme.MyNumSetTheme import kotlinx.coroutines.launch import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.clickable import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.ui.window.Dialog import java.time.Instant import java.time.LocalDate import java.time.LocalTime import java.time.ZoneId import android.content.ContentValues import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.provider.MediaStore import android.view.ViewGroup import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.runtime.saveable.rememberSaveable import kotlinx.coroutines.withContext import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.formatter.ValueFormatter import com.github.mikephil.charting.highlight.Highlight import androidx.compose.ui.viewinterop.AndroidView import com.github.mikephil.charting.components.Legend import com.github.mikephil.charting.components.LimitLine import androidx.compose.ui.graphics.Brush import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.res.painterResource import org.apache.poi.hssf.usermodel.HSSFWorkbook import java.text.SimpleDateFormat import android.os.Environment import java.io.File import java.io.FileOutputStream import java.util.* import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import android.content.Intent // 在原有类中添加数据库帮助类(放在MainActivity类外) class RecordDbHelper(context: Context) : SQLiteOpenHelper( context, DATABASE_NAME, null, DATABASE_VERSION ) { init { // 添加连接池配置 setWriteAheadLoggingEnabled(true) writableDatabase.enableWriteAheadLogging() } companion object { // 定义数据库的名称和版本 private const val DATABASE_NAME = "health_records.db" private const val DATABASE_VERSION = 1 } // 创建数据库表 override fun onCreate(db: SQLiteDatabase) { // 执行SQL语句创建records表 db.execSQL(""" CREATE TABLE records ( date TEXT NOT NULL, time TEXT NOT NULL, high INTEGER NOT NULL, low INTEGER NOT NULL, heart_rate INTEGER NOT NULL, status TEXT NOT NULL, PRIMARY KEY (date, time) ) """) } // 升级数据库 override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { // 如果存在records表,则删除 db.execSQL("DROP TABLE IF EXISTS records") // 重新创建数据库表 onCreate(db) } } // 定义一个数据类 HealthRecord,用于存储健康记录信息 data class HealthRecord( // 日期字段,存储记录的日期,类型为 String val date: String, // 时间字段,存储记录的时间,类型为 String val time: String, // 高血压字段,存储高压值,类型为 Int val high: Int, // 低压字段,存储低压值,类型为 Int val low: Int, // 心率字段,存储心率值,类型为 Int val heartRate: Int, // 状态字段,存储健康状态描述,类型为 String val status: String ) class MainActivity : ComponentActivity() { // 原色 val originalGreen = Color(0xFFE8F5E9) // 加深方案(任选其一) val deepGreen1 = Color(0xFFC8E6C9) // 浅灰绿(比原色深10%) val deepGreen2 = Color(0xFFA5D6A7) // 中等青绿(Material Teal 200) val deepGreen3 = Color(0xFF80CBC4) // 深青蓝色(Material Teal 300) val gradientColors = listOf(deepGreen2, deepGreen3) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MyNumSetTheme { BottomNavigationExample() } } } } @Composable fun NumberPicker( modifier: Modifier = Modifier, initialValue: Int = 0, range: IntRange = 20..200, onValueChange: (Int) -> Unit ) { val itemHeight = 50.dp val startOffset = (initialValue - range.first).coerceIn(0 until range.count()) //Log.d("initialValue=", "读取成功: $initialValue") val listState = rememberLazyListState(initialFirstVisibleItemIndex = startOffset) val coroutineScope = rememberCoroutineScope() val itemHeightPx = with(LocalDensity.current) { itemHeight.toPx() } // 实时计算当前选中项索引 val selectedIndex by remember { derivedStateOf { val offset = listState.firstVisibleItemScrollOffset val index = listState.firstVisibleItemIndex if (offset > itemHeightPx / 2) index + 1 else index } } // 当滚动停止时,吸附到中间项并回调 LaunchedEffect(selectedIndex, listState.isScrollInProgress) { if (!listState.isScrollInProgress) { coroutineScope.launch { listState.animateScrollToItem(selectedIndex) } onValueChange((range.first + selectedIndex)) } } LazyColumn( state = listState, modifier = modifier .height(itemHeight * 3) .width(100.dp), horizontalAlignment = Alignment.CenterHorizontally, contentPadding = PaddingValues(vertical = itemHeight), flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) ) { items(range.count()) { index -> val value = range.first + index val isSelected = selectedIndex == index Text( text = value.toString(), fontSize = if (isSelected) 32.sp else 20.sp, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, color = if (isSelected) Color.Black else Color.Gray, modifier = Modifier .height(itemHeight) .fillMaxWidth(), textAlign = TextAlign.Center ) } } } // 定义 DataStore 的扩展属性,用于访问应用的设置数据存储 val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(name = "my_settings") // PreferencesKeys 是一个单例对象,用于定义 DataStore 的键 private object PreferencesKeys { val NumbGy = intPreferencesKey("saved_gaoya") val NumbDy = intPreferencesKey("saved_diya") val NumbXl = intPreferencesKey("saved_xinlv") } @Composable fun NumberPickerDemo(dbHelper: RecordDbHelper) { val context = LocalContext.current val scope = rememberCoroutineScope() // 新增协程作用域 //val dbHelper = remember { RecordDbHelper(context) } val coroutineScope = rememberCoroutineScope() var currentStatusText by remember { mutableStateOf("") } var currentStatusColor by remember { mutableStateOf(Color.Black) } var lastClickTime by remember { mutableStateOf(0L) } // 添加两个新状态 var selectedNumberGy by remember { mutableStateOf<Int?>(null) } var selectedNumberDy by remember { mutableStateOf<Int?>(null) } var selectedNumberXl by remember { mutableStateOf<Int?>(null) } // 记住是否显示日期选择器的状态 var showDatePicker by remember { mutableStateOf(false) } // 记住是否显示时间选择器的状态 var showTimePicker by remember { mutableStateOf(false) } // 记住当前选中的日期和时间 var selectedDateTime by remember { mutableStateOf(LocalDateTime.now()) } // 日期格式化器 val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.getDefault()) // 时间格式化器 val timeFormatter = DateTimeFormatter.ofPattern("HH:mm", Locale.getDefault()) LaunchedEffect(Unit) { try { val preferences = context.settingsDataStore.data.first() selectedNumberGy = preferences[PreferencesKeys.NumbGy] ?: 100 selectedNumberDy = preferences[PreferencesKeys.NumbDy] ?: 80 selectedNumberXl = preferences[PreferencesKeys.NumbXl] ?: 75 } catch (e: Exception) { Log.e("NumberPickerDemo", "读取设置失败", e) } } Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { // 显示血压状态 selectedNumberGy?.let { gy -> selectedNumberDy?.let { dy -> val (statusText, statusColor) = run { // 分别判断高压和低压的等级 val gyLevel = when { gy < 90 -> 1 gy in 90 until 120 -> 2 gy in 120 until 140 -> 3 gy in 140 until 160 -> 4 gy in 160 until 180 -> 5 gy >= 180 -> 6 else -> 0 } val dyLevel = when { dy < 60 -> 1 dy in 60 until 80 -> 2 dy in 80 until 90 -> 3 dy in 90 until 100 -> 4 dy in 100 until 110 -> 5 dy >= 110 -> 6 else -> 0 } // 取最高等级作为最终结果 when (maxOf(gyLevel, dyLevel)) { 1 -> "低血压" to Color.Blue 2 -> "正常血压" to Color(0xFF006400) 3 -> "正常高值血压" to Color(0xFFFFB300) 4 -> "1级高血压" to Color(0xFFFF6700) 5 -> "2级高血压" to Color.Red 6 -> "3级高血压" to Color.Red else -> "****" to Color.Black } }.also { currentStatusText = it.first currentStatusColor = it.second } Card( modifier = Modifier .padding(top = 50.dp, bottom = 50.dp) .size(width = 350.dp, height = 140.dp) .background( brush = Brush.verticalGradient( colors = listOf( Color.White.copy(alpha = 0.3f), Color.Transparent ), startY = 0f, endY = 350f ), shape = RoundedCornerShape(24.dp) ), shape = RoundedCornerShape(24.dp), elevation = CardDefaults.cardElevation(4.dp) ) { val density = LocalDensity.current Text( text = statusText, fontSize = 50.sp, color = statusColor, style = LocalTextStyle.current.copy( shadow = with(density) { Shadow( color = Color.Black.copy(alpha = 0.3f), offset = Offset(2.dp.toPx(), 2.dp.toPx()), blurRadius = 4f ) } ), fontWeight = FontWeight.Bold, modifier = Modifier .fillMaxSize() .padding(24.dp), textAlign = TextAlign.Center ) } } } ?: run { CircularProgressIndicator() } selectedNumberGy?.let { gy -> selectedNumberDy?.let { dy -> selectedNumberXl?.let { xl -> // 横向排列三个选择器 Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { // 高压选择器 NumberPickerComponent( value = gy, label = "收缩压", range = 80..200, onSave = { value -> coroutineScope.launch { //delay(300) withContext(Dispatchers.Main) { selectedNumberGy = value } withContext(Dispatchers.IO) { context.settingsDataStore.edit { settings -> settings[PreferencesKeys.NumbGy] = value Log.d("NumberPickerDemo", "保存高压设置:$value") } } } } ) // 低压选择器 NumberPickerComponent( value = dy, label = "舒张压", range = 50..150, onSave = { value -> coroutineScope.launch { //delay(300) withContext(Dispatchers.Main) { selectedNumberDy = value } withContext(Dispatchers.IO) { context.settingsDataStore.edit { settings -> settings[PreferencesKeys.NumbDy] = value Log.d("NumberPickerDemo", "保存舒张压设置:$value") } } } } ) // 心率选择器 NumberPickerComponent( value = xl, label = "心率", range = 40..200, onSave = { value -> coroutineScope.launch { //delay(300) withContext(Dispatchers.Main) { selectedNumberXl = value } withContext(Dispatchers.IO) { context.settingsDataStore.edit { settings -> settings[PreferencesKeys.NumbXl] = value Log.d("NumberPickerDemo", "保存心率设置:$value") } } } } ) } val saveLock = remember { Any() } // 添加同步锁对象 Button( onClick = { val now = System.currentTimeMillis() if (now - lastClickTime > 1000) { val date = selectedDateTime.format(dateFormatter) val time = selectedDateTime.format(timeFormatter) val high = selectedNumberGy ?: return@Button val low = selectedNumberDy ?: return@Button val heartRate = selectedNumberXl ?: return@Button val status = currentStatusText lastClickTime = now synchronized(saveLock) { scope.launch(Dispatchers.IO){ try { dbHelper.writableDatabase.use { db -> val values = ContentValues().apply { put("date", date) put("time", time) put("high", high) put("low", low) put("heart_rate", heartRate) put("status", status) } db.insertWithOnConflict( "records", null, values, SQLiteDatabase.CONFLICT_REPLACE ) } withContext(Dispatchers.Main) { //delay(300) Toast.makeText(context, "记录保存成功", Toast.LENGTH_SHORT).show() } } catch (e: Exception) { Log.e("Database", "保存失败", e) } } } } }, modifier = Modifier .padding(vertical = 8.dp) .width(200.dp) // 加长按钮 .height(48.dp), // 增加高度 shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( containerColor = Color(0xFF006400), // 使用深绿色配色 contentColor = Color.White ) ) { Text( "记 录", fontSize = 22.sp, // 加大字号 fontWeight = FontWeight.Medium ) } Box( modifier = Modifier .fillMaxWidth() .padding(16.dp) .background(Color(0xFFE0F2F1), RoundedCornerShape(8.dp)) .padding(16.dp) ) { Text( text = when (currentStatusText) { "低血压" -> "收缩压<90,舒张压<60 ,一般无需治疗,若出现头晕、乏力等症状,建议就医。" "正常血压" -> "90≤收缩压<120,60≤舒张压<80 ,无需治疗,保持健康生活方式(如低盐饮食、适量运动、戒烟限酒)。" "正常高值血压" -> "120≤收缩压<140,80≤舒张压<90 ,注意饮食、运动和定期监测血压。建议生活方式干预,必要时咨询医生。" "1级高血压" -> "140≤收缩压<160,90≤舒张压<100 ,改变生活方式(如减少盐摄入、增加运动),可能需要药物治疗。" "2级高血压" -> "160≤收缩压<180,100≤舒张压<110 ,需要药物治疗并结合生活方式干预(如饮食调整、运动、减重)。" "3级高血压" -> "180≤收缩压,110≤舒张压, 需紧急医疗干预,可能需要住院治疗。通常需要联合药物治疗。" else -> "" }, color = Color.Black, fontSize = 20.sp ) } } } } ?: run { CircularProgressIndicator() } // 显示日期和时间选择器 Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ){ Text( text = "日期: ${selectedDateTime.format(dateFormatter)}", style = MaterialTheme.typography.bodyLarge, fontSize = 20.sp, // 增大字体尺寸 //fontWeight = FontWeight.Bold, // 加粗字体 color = Color.DarkGray, // 使用更深的颜色 modifier = Modifier .clickable { showDatePicker = true } .padding(8.dp) ) Text( text = "时间: ${selectedDateTime.format(timeFormatter)}", style = MaterialTheme.typography.bodyLarge, fontSize = 20.sp, // 增大字体尺寸 //fontWeight = FontWeight.Bold, // 加粗字体 color = Color.DarkGray, // 使用更深的颜色 modifier = Modifier .clickable { showTimePicker = true } .padding(8.dp) ) // 日期选择器 if (showDatePicker) { DatePickerDialog( onDismissRequest = { showDatePicker = false }, // 点击外部时关闭日期选择器 onDateSelected = { date -> // 更新选中的日期 selectedDateTime = selectedDateTime.withYear(date.year).withMonth(date.monthValue).withDayOfMonth(date.dayOfMonth) showDatePicker = false // 关闭日期选择器 } ) } // 时间选择器 if (showTimePicker) { TimePickerDialog( onDismissRequest = { showTimePicker = false }, // 点击外部时关闭时间选择器 onTimeSelected = { time -> // 更新选中的时间 selectedDateTime = selectedDateTime.withHour(time.hour).withMinute(time.minute) showTimePicker = false // 关闭时间选择器 } ) } } } } @Composable private fun NumberPickerComponent( value: Int, label: String, range: IntRange, onSave: (Int) -> Unit ) { var currentValue by remember(value) { mutableStateOf(value) } // 添加当前值状态 Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(label, fontSize = 16.sp) NumberPicker( initialValue = value, range = range, onValueChange = { newValue -> if (newValue != currentValue) { // 只在数值变化时触发保存 currentValue = newValue onSave(newValue) } } ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun DatePickerDialog( onDismissRequest: () -> Unit, onDateSelected: (LocalDate) -> Unit ) { // 获取当前日期 val currentDate = LocalDate.now() // 创建DatePicker状态,并初始化为当前日期 val datePickerState = rememberDatePickerState(initialSelectedDateMillis = currentDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()) // 创建对话框 Dialog( onDismissRequest = onDismissRequest, ) { // 使用Column布局来组织对话框内容 Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { // 显示标题 Text( text = "Select Date", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 16.dp) ) // 显示日期选择器 DatePicker(state = datePickerState) // 使用Row布局来组织按钮 Row( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp), horizontalArrangement = Arrangement.End ) { // 取消按钮 Button( onClick = onDismissRequest, modifier = Modifier.padding(end = 8.dp) ) { Text("Cancel") } // 确定按钮 Button(onClick = { // 获取选中的日期 val selectedDate = LocalDate.ofInstant( Instant.ofEpochMilli(datePickerState.selectedDateMillis ?: currentDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()), ZoneId.systemDefault() ) // 调用回调函数,传递选中的日期 onDateSelected(selectedDate) }) { Text("OK") } } } } } @OptIn(ExperimentalMaterial3Api::class) // 使用实验性API的注解 @Composable // 标记为可组合函数 fun TimePickerDialog( onDismissRequest: () -> Unit, // 对话框关闭时的回调函数 onTimeSelected: (LocalTime) -> Unit // 时间选择后的回调函数 ) { val currentTime = LocalTime.now(ZoneId.systemDefault()) // 获取当前时间 val timePickerState = rememberTimePickerState(initialHour = currentTime.hour, initialMinute = currentTime.minute) // 创建时间选择器的状态,并初始化为当前时间 Dialog( onDismissRequest = onDismissRequest, // 设置对话框关闭的回调函数 ) { Column( modifier = Modifier .fillMaxWidth() // 填充父布局的宽度 .padding(16.dp) // 设置内边距 ) { Text( text = "Select Time", // 显示文本 style = MaterialTheme.typography.headlineSmall, // 使用主题中的小标题样式 modifier = Modifier.padding(bottom = 16.dp) // 设置底部内边距 ) TimePicker(state = timePickerState) // 显示时间选择器 Row( modifier = Modifier .fillMaxWidth() // 填充父布局的宽度 .padding(top = 16.dp), // 设置顶部内边距 horizontalArrangement = Arrangement.End // 水平排列方式为靠右 ) { Button( onClick = onDismissRequest, // 点击按钮时调用关闭回调函数 modifier = Modifier.padding(end = 8.dp) // 设置右边距 ) { Text("Cancel") // 显示取消文本 } Button(onClick = { val selectedTime = LocalTime.of(timePickerState.hour, timePickerState.minute) // 获取选中的时间 onTimeSelected(selectedTime) // 调用时间选择后的回调函数 }) { Text("OK") // 显示确定文本 } } } } } @Composable fun BottomNavigationExample() { val context = LocalContext.current val activity = context as Activity // 直接转换为 Activity val dbHelper = remember { RecordDbHelper(context) } var selectedItem by rememberSaveable { mutableStateOf(0) } val historyStack = remember { mutableStateListOf<Int>() } val originalGreen = Color(0xFFE8F5E9) // 加深方案(任选其一) val deepGreen1 = Color(0xFFC8E6C9) // 浅灰绿(比原色深10%) val deepGreen2 = Color(0xFFA5D6A7) // 中等青绿(Material Teal 200) val deepGreen3 = Color(0xFF80CBC4) // 深青蓝色(Material Teal 300) val gradientColors = listOf(deepGreen2, deepGreen3) // 处理返回键 BackHandler(enabled = historyStack.isNotEmpty()) { if (historyStack.isNotEmpty()) { // 兼容性写法(支持所有 Kotlin 版本) val lastIndex = historyStack.size - 1 selectedItem = historyStack.removeAt(lastIndex) } else { activity.finish() // 正确调用 Activity 的 finish() } } // 点击底部导航项时更新历史栈 fun onTabSelected(index: Int) { historyStack.add(selectedItem) selectedItem = index } val items = listOf("记录", "历史", "图表") // 使用Scaffold布局,包含底部导航栏 Scaffold( bottomBar = { // 创建底部导航栏 NavigationBar( modifier = Modifier //.height(72.dp) // 保持高度设置 .padding(horizontal = 8.dp) .background( brush = Brush.linearGradient( colors = gradientColors, start = Offset(0f, 0f), end = Offset(1000f, 0f) ), alpha = 0.9f // 添加背景透明度 ), containerColor = Color.Transparent ) { // 遍历items列表,为每个项创建一个NavigationBarItem items.forEachIndexed { index, item -> NavigationBarItem( // 根据index选择不同的图标 icon = { Icon( painter = painterResource(id = when (index) { 0 -> R.drawable.write // 修正资源引用(去掉_png后缀) 1 -> R.drawable.list // 修正资源引用(去掉_png后缀) else -> R.drawable.chart2 }), contentDescription = item, modifier = Modifier .size(48.dp), //.background(Color.Red) tint = Color.Unspecified ) }, // 显示项的标签 label = { Text( item, fontSize = 12.sp, color = Color.Black, // 强制文字颜色为白色 modifier = Modifier.padding(top = 0.dp) //modifier = Modifier.padding(vertical = 2.dp) ) }, // 判断当前项是否被选中 selected = selectedItem == index, onClick = { onTabSelected(index) }, colors = NavigationBarItemDefaults.colors( selectedIconColor = Color.White, indicatorColor = Color(0xFF006400).copy(alpha = 0.5f) ) ) } } } ) { innerPadding -> // 根据选中的项显示不同的内容 when (selectedItem) { 0 -> Box(modifier = Modifier .fillMaxSize() .background( brush = Brush.verticalGradient( colors = gradientColors, startY = 0f, endY = 1200f ) ) .padding(innerPadding) // 添加内边距 ) { NumberPickerDemo(dbHelper = dbHelper) } 1 -> Box(modifier = Modifier .fillMaxSize() .background( brush = Brush.verticalGradient( // 历史界面也改为渐变背景 colors = gradientColors, startY = 0f, endY = 1200f ) ) .padding(top = 20.dp) // 新增顶部内边距 .padding(innerPadding) // 添加内边距 ){ HistoryScreen(dbHelper = dbHelper) } 2 -> Box(modifier = Modifier .fillMaxSize() .background( brush = Brush.verticalGradient( // 统计界面改为渐变背景 colors = gradientColors, startY = 0f, endY = 1200f ) ) .padding(innerPadding) // 添加内边距 ){ StatisticsScreen(dbHelper = dbHelper) } } } } @Composable fun HistoryScreen(dbHelper: RecordDbHelper) { val records = remember { mutableStateListOf<HealthRecord>() } var showDeleteDialog by remember { mutableStateOf(false) } var selectedRecord by remember { mutableStateOf<HealthRecord?>(null) } val scope = rememberCoroutineScope() // 加载数据库记录 LaunchedEffect(Unit) { scope.launch(Dispatchers.IO) { try { val loadedRecords = mutableListOf<HealthRecord>() dbHelper.readableDatabase.use { db -> val cursor = db.query( "records", arrayOf("date", "time", "high", "low", "heart_rate", "status"), null, null, null, null, "date DESC, time DESC" ) try { while (cursor.moveToNext()) { val date = cursor.getString(0) ?: "" val time = cursor.getString(1) ?: "" val high = cursor.getInt(2) val low = cursor.getInt(3) val heartRate = cursor.getInt(4) val status = cursor.getString(5) ?: "未知" loadedRecords.add( HealthRecord(date, time, high, low, heartRate, status) ) } } finally { cursor.close() } } withContext(Dispatchers.Main) { records.clear() records.addAll(loadedRecords) } } catch (e: Exception) { Log.e("HistoryScreen", "加载记录失败: ${e.message}", e) } } } // 删除确认对话框 if (showDeleteDialog) { AlertDialog( onDismissRequest = { showDeleteDialog = false }, title = { Text("删除记录") }, text = { Text("确定要删除这条记录吗?") }, confirmButton = { TextButton( onClick = { selectedRecord?.let { record -> scope.launch(Dispatchers.IO) { try { dbHelper.writableDatabase.delete( "records", "date = ? AND time = ?", arrayOf(record.date, record.time) ) withContext(Dispatchers.Main) { records.remove(record) showDeleteDialog = false } } catch (e: Exception) { Log.e("HistoryScreen", "删除失败: ${e.message}", e) } } } } ) { Text("确认") } }, dismissButton = { TextButton( onClick = { showDeleteDialog = false } ) { Text("取消") } } ) } // 记录列表 LazyColumn( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { items( count = records.size, key = { index -> "${records[index].date}_${records[index].time}" } ) { index -> val record = records[index] Card( modifier = Modifier .fillMaxWidth(0.9f) .padding(vertical = 2.dp) .clickable { selectedRecord = record showDeleteDialog = true }, elevation = CardDefaults.cardElevation(4.dp), colors = CardDefaults.cardColors( containerColor = when (record.status) { "低血压" -> Color(0xFFE3F2FD) "正常血压" -> Color(0xFFE8F5E9) "正常高值血压" -> Color(0xFFFFF8E1) "1级高血压" -> Color(0xFFFFF3E0) "2级高血压", "3级高血压" -> Color(0xFFFFEBEE) else -> Color.White } ) ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp) ) { Text( "日期: ${record.date} ${record.time}", fontSize = 16.sp, modifier = Modifier.weight(1f) ) Text( "状态: ${record.status}", color = when (record.status) { "低血压" -> Color.Blue "正常血压" -> Color(0xFF006400) "正常高值血压" -> Color(0xFFFFB300) "1级高血压" -> Color(0xFFFF6700) "2级高血压", "3级高血压" -> Color.Red else -> Color.Gray }, fontSize = 18.sp ) } Row( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp) ) { Text( "血压: ${record.high}/${record.low} mmHg", fontSize = 18.sp, modifier = Modifier.weight(1f) ) Text( "心率: ${record.heartRate} 次/分钟", fontSize = 18.sp ) } } } } } } private suspend fun exportToExcel(context: Context, dbHelper: RecordDbHelper) { val workbook = HSSFWorkbook() val sheet = workbook.createSheet("血压记录") try { // 创建表头 val headerTitles = arrayOf("日期", "时间", "收缩压", "舒张压", "心率", "状态") val headerRow = sheet.createRow(0) headerTitles.forEachIndexed { index, title -> headerRow.createCell(index).setCellValue(title) } // 查询数据库 dbHelper.readableDatabase.query("records", arrayOf("date", "time", "high", "low", "heart_rate", "status"), null, null, null, null, null ).use { cursor -> var rowNum = 1 while (cursor.moveToNext()) { val row = sheet.createRow(rowNum++) row.createCell(0).setCellValue(cursor.getString(0)) // date row.createCell(1).setCellValue(cursor.getString(1)) // time row.createCell(2).setCellValue(cursor.getInt(2).toDouble()) // high row.createCell(3).setCellValue(cursor.getInt(3).toDouble()) // low row.createCell(4).setCellValue(cursor.getInt(4).toDouble()) // heart row.createCell(5).setCellValue(cursor.getString(5)) // status } } // 生成文件名 val fileName = "BP_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date())}.xls" // 保存到应用专属目录 val docsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) val file = File(docsDir, fileName) FileOutputStream(file).use { fos -> workbook.write(fos) withContext(Dispatchers.Main) { // 添加协程上下文切换 Toast.makeText(context, "导出成功:${file.absolutePath}", Toast.LENGTH_LONG).show() } } } catch (e: Exception) { Log.e("ExcelExport", "导出失败", e) withContext(Dispatchers.Main) { // 添加协程上下文切换 Toast.makeText(context, "导出失败:${e.localizedMessage}", Toast.LENGTH_SHORT).show() } } finally { workbook.close() } } private suspend fun importFromExcel(context: Context, dbHelper: RecordDbHelper, uri: android.net.Uri) { try { val inputStream = context.contentResolver.openInputStream(uri) val workbook = HSSFWorkbook(inputStream) val sheet = workbook.getSheetAt(0) var successCount = 0 var errorCount = 0 dbHelper.writableDatabase.use { db -> // 开启事务提高导入性能 db.beginTransaction() try { for (row in sheet) { if (row.rowNum == 0) continue // 跳过表头 try { val date = row.getCell(0)?.stringCellValue ?: "" val time = row.getCell(1)?.stringCellValue ?: "" val high = row.getCell(2)?.numericCellValue?.toInt() ?: 0 val low = row.getCell(3)?.numericCellValue?.toInt() ?: 0 val heartRate = row.getCell(4)?.numericCellValue?.toInt() ?: 0 val status = row.getCell(5)?.stringCellValue ?: "" // 数据校验 if (date.isBlank() || time.isBlank() || high < 50 || low < 30) { errorCount++ continue } val values = ContentValues().apply { put("date", date) put("time", time) put("high", high) put("low", low) put("heart_rate", heartRate) put("status", status) } db.insertWithOnConflict( "records", null, values, SQLiteDatabase.CONFLICT_REPLACE ) successCount++ } catch (e: Exception) { errorCount++ Log.e("ExcelImport", "第${row.rowNum}行导入失败: ${e.message}") } } db.setTransactionSuccessful() } finally { db.endTransaction() } } withContext(Dispatchers.Main) { Toast.makeText( context, "导入完成:成功 $successCount 条,失败 $errorCount 条", Toast.LENGTH_LONG ).show() } } catch (e: Exception) { withContext(Dispatchers.Main) { Toast.makeText(context, "导入失败:${e.localizedMessage}", Toast.LENGTH_SHORT).show() } Log.e("ExcelImport", "导入失败", e) } } @Composable fun StatisticsScreen(dbHelper: RecordDbHelper) { val context = LocalContext.current val records = remember { mutableStateListOf<HealthRecord>() } val isLoading = remember { mutableStateOf(true) } val selectedRecord = remember { mutableStateOf<HealthRecord?>(null) } // 新增选中状态 val scope = rememberCoroutineScope() val filePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocument() ) { uri -> uri?.let { context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION) scope.launch(Dispatchers.IO) { importFromExcel(context, dbHelper, it) } } } Column(modifier = Modifier.fillMaxSize()) { // 新增按钮行 Row( modifier = Modifier .fillMaxWidth() .padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly ) { Button( onClick = { scope.launch(Dispatchers.IO) { exportToExcel(context, dbHelper) } }, colors = ButtonDefaults.buttonColors( containerColor = Color(0xFF006400), contentColor = Color.White ) ) { Text("导出数据") } Button( onClick = { filePickerLauncher.launch(arrayOf("application/vnd.ms-excel")) }, colors = ButtonDefaults.buttonColors( containerColor = Color(0xFF006400), contentColor = Color.White ) ) { Text("导入数据") } } selectedRecord.value?.let { record -> Card( modifier = Modifier .fillMaxWidth() .padding(8.dp), elevation = CardDefaults.cardElevation(4.dp) ) { Column(modifier = Modifier.padding(16.dp)) { //Text("选中记录详情", style = MaterialTheme.typography.headlineSmall) //Spacer(modifier = Modifier.height(8.dp)) Text("时间: ${record.date} ${record.time}") Text("血压: ${record.high}/${record.low} mmHg") Text("心率: ${record.heartRate} 次/分") } } } ?: run { Text( text = "点击图表查看详细数据", modifier = Modifier.padding(16.dp), color = Color.Gray ) } LaunchedEffect(Unit) { withContext(Dispatchers.IO) { val tempList = dbHelper.readableDatabase.use { db -> db.query( "records", arrayOf("date", "time", "high", "low", "heart_rate"), null, null, null, null, "date || time ASC" ).use { cursor -> val list = mutableListOf<HealthRecord>() while (cursor.moveToNext()) { list.add( HealthRecord( date = cursor.getString(0), time = cursor.getString(1), high = cursor.getInt(2), low = cursor.getInt(3), heartRate = cursor.getInt(4), status = "" ) ) } list } } withContext(Dispatchers.Main) { records.clear() records.addAll(tempList) isLoading.value = false } } } // 加载指示器 if (isLoading.value) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } return } // 图表视图 AndroidView( factory = { context -> LineChart(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) setTouchEnabled(true) setPinchZoom(true) description.isEnabled = false // X轴配置 xAxis.apply { position = XAxis.XAxisPosition.BOTTOM granularity = 1f labelRotationAngle = -60f //setLabelCount(10, true) valueFormatter = object : ValueFormatter() { override fun getFormattedValue(value: Float): String { return records.getOrNull(value.toInt())?.let { "${it.date}\n${it.time}" } ?: "" } } setLabelCount(10, true) } // Y轴配置 axisLeft.apply { granularity = 20f axisMinimum = 0f axisMaximum = 220f addLimitLine(LimitLine(90f, "收缩压阈值").apply { lineColor = Color.Red.hashCode() lineWidth = 2f enableDashedLine(10f, 10f, 0f) }) addLimitLine(LimitLine(140f, "舒张压阈值").apply { lineColor = Color.Red.hashCode() lineWidth = 2f enableDashedLine(10f, 10f, 0f) }) } axisRight.isEnabled = false legend.apply { verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM horizontalAlignment = Legend.LegendHorizontalAlignment.CENTER orientation = Legend.LegendOrientation.HORIZONTAL setDrawInside(false) yOffset = 20f } setOnChartValueSelectedListener(object : com.github.mikephil.charting.listener.OnChartValueSelectedListener { // 实现这两个方法修复错误 override fun onValueSelected(e: Entry?, h: Highlight?) { e?.let { val index = it.x.toInt() if (index in records.indices) { selectedRecord.value = records[index] } } } override fun onNothingSelected() { selectedRecord.value = null } }) } }, update = { chart -> try { if (records.isNotEmpty()) { val highEntries = mutableListOf<Entry>() val lowEntries = mutableListOf<Entry>() val heartEntries = mutableListOf<Entry>() records.forEachIndexed { index, record -> highEntries.add(Entry(index.toFloat(), record.high.toFloat())) lowEntries.add(Entry(index.toFloat(), record.low.toFloat())) heartEntries.add(Entry(index.toFloat(), record.heartRate.toFloat())) } val highSet = LineDataSet(highEntries, "收缩压").apply { color = Color.Red.hashCode() lineWidth = 2f setDrawCircles(false) } val lowSet = LineDataSet(lowEntries, "舒张压").apply { color = Color.Blue.hashCode() lineWidth = 2f setDrawCircles(false) } val heartSet = LineDataSet(heartEntries, "心率").apply { color = Color.Green.hashCode() lineWidth = 2f setDrawCircles(false) } chart.data = LineData(highSet, lowSet, heartSet) chart.animateY(1000) chart.invalidate() chart.setVisibleXRangeMaximum(15f) chart.moveViewToX((records.size - 1).toFloat()) } } catch (e: Exception) { Log.e("ChartUpdate", "更新图表失败: ${e.message}", e) } }, modifier = Modifier .fillMaxSize() .padding(16.dp) ) } }
AndroidManifest.xml如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:requestLegacyExternalStorage="true" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MyNumSet" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.MyNumSet"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
build.gradle.kts如下:
plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) } android { namespace = "com.example.mynumset" compileSdk = 35 defaultConfig { applicationId = "com.example.mynumset" minSdk = 34 targetSdk = 35 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" } buildFeatures { compose = true } } dependencies { implementation("androidx.compose.runtime:runtime:1.7.8") implementation("androidx.activity:activity:1.10.1") implementation ("org.apache.poi:poi-ooxml:5.4.1") implementation ("org.apache.poi:poi:5.4.1") implementation ("com.fasterxml.jackson.core:jackson-core:2.13.0") implementation ("androidx.multidex:multidex:2.0.1") // 添加multidex支持 implementation ("com.github.PhilJay:MPAndroidChart:v3.1.0") implementation ("androidx.datastore:datastore-preferences:1.0.0") implementation("androidx.compose.foundation:foundation:1.4.0") implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) }