fix: 修复自定义日期时区并统一用量入口
This commit is contained in:
parent
1d98d4dee1
commit
45c8f71b8a
@ -299,7 +299,11 @@ private fun rememberDashboardItems(
|
|||||||
DashboardCard(
|
DashboardCard(
|
||||||
card = card,
|
card = card,
|
||||||
onClick = { onOpenService(card.service) },
|
onClick = { onOpenService(card.service) },
|
||||||
onOpenCcgoUsageDetail = if (card.service == ServiceType.COMMANDCODE_GO) onOpenCcgoUsageDetail else null
|
onOpenUsageDetail = when (card.service) {
|
||||||
|
ServiceType.OPENCODE_GO -> onOpenUsageDetail
|
||||||
|
ServiceType.COMMANDCODE_GO -> onOpenCcgoUsageDetail
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -544,7 +548,7 @@ private fun DraggableDashboardCards(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit, onOpenCcgoUsageDetail: (() -> Unit)? = null) {
|
private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit, onOpenUsageDetail: (() -> Unit)? = null) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -578,10 +582,10 @@ private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit, onOpenCcgo
|
|||||||
// ─── 主体:服务特定的主信息 ───
|
// ─── 主体:服务特定的主信息 ───
|
||||||
BalanceMainArea(card)
|
BalanceMainArea(card)
|
||||||
|
|
||||||
// CCGO 卡片底部:用量详情入口(仅当有凭证且非 CCGO 未配置状态时显示)
|
// 用量服务卡片底部:用量详情入口(仅当有凭证时显示)
|
||||||
if (onOpenCcgoUsageDetail != null && card.credentialState != com.rainy.token.domain.model.CredentialStatus.State.NOT_CONFIGURED) {
|
if (onOpenUsageDetail != null && card.credentialState != com.rainy.token.domain.model.CredentialStatus.State.NOT_CONFIGURED) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
TextButton(onClick = onOpenCcgoUsageDetail) {
|
TextButton(onClick = onOpenUsageDetail) {
|
||||||
Text("查看用量详情", color = StrawberryPink)
|
Text("查看用量详情", color = StrawberryPink)
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@ -64,6 +64,9 @@ import com.rainy.token.data.local.ChartGranularity
|
|||||||
import com.rainy.token.ui.theme.InkMuted
|
import com.rainy.token.ui.theme.InkMuted
|
||||||
import com.rainy.token.ui.theme.StrawberryPink
|
import com.rainy.token.ui.theme.StrawberryPink
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@ -103,7 +106,7 @@ fun UsageChartScreen(
|
|||||||
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
||||||
var showCustomRangeStart by remember { mutableStateOf(false) }
|
var showCustomRangeStart by remember { mutableStateOf(false) }
|
||||||
var showCustomRangeEnd by remember { mutableStateOf(false) }
|
var showCustomRangeEnd by remember { mutableStateOf(false) }
|
||||||
var customRangeStartMs by remember { mutableStateOf(0L) }
|
var customRangeStartDate by remember { mutableStateOf<LocalDate?>(null) }
|
||||||
var showCostDetail by remember { mutableStateOf(false) }
|
var showCostDetail by remember { mutableStateOf(false) }
|
||||||
var showReqDetail by remember { mutableStateOf(false) }
|
var showReqDetail by remember { mutableStateOf(false) }
|
||||||
var showTokenDetail by remember { mutableStateOf(false) }
|
var showTokenDetail by remember { mutableStateOf(false) }
|
||||||
@ -364,10 +367,7 @@ fun UsageChartScreen(
|
|||||||
DateTimePickerDialog(
|
DateTimePickerDialog(
|
||||||
title = "选择日期",
|
title = "选择日期",
|
||||||
onConfirm = { ms ->
|
onConfirm = { ms ->
|
||||||
val utc = java.time.ZoneOffset.UTC
|
viewModel.setCustomDay(ms.toUtcLocalDate())
|
||||||
val dayStart = java.time.Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate()
|
|
||||||
.atStartOfDay(utc).toInstant().toEpochMilli()
|
|
||||||
viewModel.setCustomDayRange(dayStart, dayStart + 86400_000L - 1)
|
|
||||||
showCustomDayPicker = false
|
showCustomDayPicker = false
|
||||||
},
|
},
|
||||||
onDismiss = { showCustomDayPicker = false }
|
onDismiss = { showCustomDayPicker = false }
|
||||||
@ -377,7 +377,7 @@ fun UsageChartScreen(
|
|||||||
DateTimePickerDialog(
|
DateTimePickerDialog(
|
||||||
title = "选择月份(任意一天)",
|
title = "选择月份(任意一天)",
|
||||||
onConfirm = { ms ->
|
onConfirm = { ms ->
|
||||||
viewModel.setCustomMonth(ms)
|
viewModel.setCustomMonth(ms.toUtcLocalDate())
|
||||||
showCustomMonthPicker = false
|
showCustomMonthPicker = false
|
||||||
},
|
},
|
||||||
onDismiss = { showCustomMonthPicker = false }
|
onDismiss = { showCustomMonthPicker = false }
|
||||||
@ -387,7 +387,7 @@ fun UsageChartScreen(
|
|||||||
DateTimePickerDialog(
|
DateTimePickerDialog(
|
||||||
title = "开始日期",
|
title = "开始日期",
|
||||||
onConfirm = { ms ->
|
onConfirm = { ms ->
|
||||||
customRangeStartMs = ms
|
customRangeStartDate = ms.toUtcLocalDate()
|
||||||
showCustomRangeStart = false
|
showCustomRangeStart = false
|
||||||
showCustomRangeEnd = true
|
showCustomRangeEnd = true
|
||||||
},
|
},
|
||||||
@ -398,13 +398,12 @@ fun UsageChartScreen(
|
|||||||
DateTimePickerDialog(
|
DateTimePickerDialog(
|
||||||
title = "结束日期",
|
title = "结束日期",
|
||||||
onConfirm = { ms ->
|
onConfirm = { ms ->
|
||||||
val utc = java.time.ZoneOffset.UTC
|
val startDate = customRangeStartDate ?: return@DateTimePickerDialog
|
||||||
val startDay = java.time.Instant.ofEpochMilli(customRangeStartMs).atOffset(utc).toLocalDate()
|
val endDate = ms.toUtcLocalDate()
|
||||||
.atStartOfDay(utc).toInstant().toEpochMilli()
|
if (!endDate.isBefore(startDate)) {
|
||||||
val endDay = java.time.Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate()
|
viewModel.setCustomRange(startDate, endDate)
|
||||||
.atStartOfDay(utc).toInstant().toEpochMilli() + 86400_000L - 1
|
showCustomRangeEnd = false
|
||||||
viewModel.setCustomRange(startDay, endDay)
|
}
|
||||||
showCustomRangeEnd = false
|
|
||||||
},
|
},
|
||||||
onDismiss = { showCustomRangeEnd = false }
|
onDismiss = { showCustomRangeEnd = false }
|
||||||
)
|
)
|
||||||
@ -486,6 +485,9 @@ fun UsageChartScreen(
|
|||||||
// 图表卡片容器
|
// 图表卡片容器
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
private fun Long.toUtcLocalDate(): LocalDate =
|
||||||
|
Instant.ofEpochMilli(this).atOffset(ZoneOffset.UTC).toLocalDate()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ChartCard(
|
internal fun ChartCard(
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
@ -157,33 +158,32 @@ class UsageChartViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun setCustomDay(date: LocalDate) {
|
||||||
fun setCustomDayRange(from: Long, to: Long) {
|
|
||||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_DAY_HOURLY) }
|
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_DAY_HOURLY) }
|
||||||
_customDayRange = from to to
|
_customDay = date
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCustomMonth(monthStartMs: Long) {
|
fun setCustomMonth(monthDate: LocalDate) {
|
||||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_MONTH_DAILY) }
|
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_MONTH_DAILY) }
|
||||||
_customMonthStart = monthStartMs
|
_customMonth = monthDate.withDayOfMonth(1)
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCustomRange(from: Long, to: Long) {
|
fun setCustomRange(fromDate: LocalDate, toDate: LocalDate) {
|
||||||
|
if (toDate.isBefore(fromDate)) return
|
||||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_RANGE_DAILY) }
|
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_RANGE_DAILY) }
|
||||||
_customRange = from to to
|
_customRange = fromDate to toDate
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _customDayRange: Pair<Long, Long> = 0L to 0L
|
private var _customDay: LocalDate? = null
|
||||||
private var _customMonthStart: Long = 0L
|
private var _customMonth: LocalDate? = null
|
||||||
private var _customRange: Pair<Long, Long> = 0L to 0L
|
private var _customRange: Pair<LocalDate, LocalDate>? = null
|
||||||
|
|
||||||
private fun timeRangeFor(g: ChartGranularity): Pair<Long?, Long?> {
|
private fun timeRangeFor(g: ChartGranularity): Pair<Long?, Long?> {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val useUtc8 = _state.value.useUtc8
|
val zoneOffset = if (_state.value.useUtc8) ZoneOffset.ofHours(8) else ZoneOffset.UTC
|
||||||
val zoneOffset = if (useUtc8) ZoneOffset.ofHours(8) else ZoneOffset.UTC
|
|
||||||
val today = Instant.ofEpochMilli(now).atOffset(zoneOffset).toLocalDate()
|
val today = Instant.ofEpochMilli(now).atOffset(zoneOffset).toLocalDate()
|
||||||
val todayMidnight = today.atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
val todayMidnight = today.atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
||||||
return when (g) {
|
return when (g) {
|
||||||
@ -192,37 +192,36 @@ class UsageChartViewModel @Inject constructor(
|
|||||||
ChartGranularity.LAST_24H_HOURLY -> now - 24 * 3600_000L to null
|
ChartGranularity.LAST_24H_HOURLY -> now - 24 * 3600_000L to null
|
||||||
ChartGranularity.TODAY_HOURLY -> todayMidnight to todayMidnight + 86400_000L - 1
|
ChartGranularity.TODAY_HOURLY -> todayMidnight to todayMidnight + 86400_000L - 1
|
||||||
ChartGranularity.YESTERDAY_HOURLY -> todayMidnight - 86400_000L to todayMidnight - 1
|
ChartGranularity.YESTERDAY_HOURLY -> todayMidnight - 86400_000L to todayMidnight - 1
|
||||||
ChartGranularity.LAST_7D_DAILY -> {
|
ChartGranularity.LAST_7D_DAILY -> todayMidnight - 6 * 86400_000L to todayMidnight + 86400_000L - 1
|
||||||
todayMidnight - 6 * 86400_000L to todayMidnight + 86400_000L - 1
|
ChartGranularity.CUSTOM_DAY_HOURLY -> dayRange(_customDay ?: today, zoneOffset)
|
||||||
}
|
ChartGranularity.THIS_MONTH_DAILY -> monthRange(today.withDayOfMonth(1), zoneOffset)
|
||||||
ChartGranularity.CUSTOM_DAY_HOURLY -> {
|
|
||||||
if (_customDayRange.first == 0L) todayMidnight to todayMidnight + 86400_000L - 1
|
|
||||||
else _customDayRange.first to _customDayRange.second
|
|
||||||
}
|
|
||||||
ChartGranularity.THIS_MONTH_DAILY -> {
|
|
||||||
val firstOfMonth = today.withDayOfMonth(1).atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
|
||||||
val lastOfMonth = today.withDayOfMonth(today.lengthOfMonth())
|
|
||||||
.atStartOfDay(zoneOffset).toInstant().toEpochMilli() + 86400_000L - 1
|
|
||||||
firstOfMonth to lastOfMonth
|
|
||||||
}
|
|
||||||
ChartGranularity.CUSTOM_MONTH_DAILY -> {
|
ChartGranularity.CUSTOM_MONTH_DAILY -> {
|
||||||
if (_customMonthStart == 0L) null to null
|
val month = _customMonth ?: return null to null
|
||||||
else {
|
monthRange(month, zoneOffset)
|
||||||
val targetMonth = Instant.ofEpochMilli(_customMonthStart).atOffset(zoneOffset).toLocalDate()
|
|
||||||
.withDayOfMonth(1)
|
|
||||||
val firstOfMonth = targetMonth.atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
|
||||||
val lastOfMonth = targetMonth.withDayOfMonth(targetMonth.lengthOfMonth())
|
|
||||||
.atStartOfDay(zoneOffset).toInstant().toEpochMilli() + 86400_000L - 1
|
|
||||||
firstOfMonth to lastOfMonth
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ChartGranularity.CUSTOM_RANGE_DAILY -> {
|
ChartGranularity.CUSTOM_RANGE_DAILY -> {
|
||||||
if (_customRange.first == 0L) null to null
|
val range = _customRange ?: return null to null
|
||||||
else _customRange.first to _customRange.second
|
val from = range.first.atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
||||||
|
val to = range.second.plusDays(1).atStartOfDay(zoneOffset).toInstant().toEpochMilli() - 1
|
||||||
|
from to to
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun dayRange(date: LocalDate, zoneOffset: ZoneOffset): Pair<Long, Long> {
|
||||||
|
val from = date.atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
||||||
|
val to = date.plusDays(1).atStartOfDay(zoneOffset).toInstant().toEpochMilli() - 1
|
||||||
|
return from to to
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun monthRange(month: LocalDate, zoneOffset: ZoneOffset): Pair<Long, Long> {
|
||||||
|
val firstDay = month.withDayOfMonth(1)
|
||||||
|
val from = firstDay.atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
||||||
|
val to = firstDay.plusMonths(1).atStartOfDay(zoneOffset).toInstant().toEpochMilli() - 1
|
||||||
|
return from to to
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun fillGaps(
|
private fun fillGaps(
|
||||||
buckets: List<ChartBucket>,
|
buckets: List<ChartBucket>,
|
||||||
granularity: ChartGranularity,
|
granularity: ChartGranularity,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SelectableDates
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@ -93,7 +94,7 @@ fun UsageDetailScreen(
|
|||||||
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
||||||
var showCustomRangeStart by remember { mutableStateOf(false) }
|
var showCustomRangeStart by remember { mutableStateOf(false) }
|
||||||
var showCustomRangeEnd by remember { mutableStateOf(false) }
|
var showCustomRangeEnd by remember { mutableStateOf(false) }
|
||||||
var customRangeStartMs by remember { mutableStateOf(0L) }
|
var customRangeStartDate by remember { mutableStateOf<LocalDate?>(null) }
|
||||||
var showCostDetail by remember { mutableStateOf(false) }
|
var showCostDetail by remember { mutableStateOf(false) }
|
||||||
var showReqDetail by remember { mutableStateOf(false) }
|
var showReqDetail by remember { mutableStateOf(false) }
|
||||||
var showTokenDetail by remember { mutableStateOf(false) }
|
var showTokenDetail by remember { mutableStateOf(false) }
|
||||||
@ -299,15 +300,33 @@ fun UsageDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 自定义日期选择器
|
// 自定义日期选择器
|
||||||
if (showCustomDayPicker) DateTimePickerDialog("选择日期", { ms ->
|
if (showCustomDayPicker) DateOnlyPickerDialog("选择日期", { date ->
|
||||||
val utc = ZoneOffset.UTC; val ds = Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate().atStartOfDay(utc).toInstant().toEpochMilli()
|
viewModel.setCustomDay(date)
|
||||||
viewModel.setCustomDayRange(ds, ds + 86400_000L - 1); showCustomDayPicker = false }, { showCustomDayPicker = false })
|
showCustomDayPicker = false
|
||||||
if (showCustomMonthPicker) DateTimePickerDialog("选择月份(任意一天)", { viewModel.setCustomMonth(it); showCustomMonthPicker = false }, { showCustomMonthPicker = false })
|
}, { showCustomDayPicker = false })
|
||||||
if (showCustomRangeStart) DateTimePickerDialog("开始日期", { customRangeStartMs = it; showCustomRangeStart = false; showCustomRangeEnd = true }, { showCustomRangeStart = false })
|
if (showCustomMonthPicker) DateOnlyPickerDialog("选择月份(任意一天)", { date ->
|
||||||
if (showCustomRangeEnd) DateTimePickerDialog("结束日期", { ms ->
|
viewModel.setCustomMonth(date)
|
||||||
val utc = ZoneOffset.UTC; val sd = Instant.ofEpochMilli(customRangeStartMs).atOffset(utc).toLocalDate().atStartOfDay(utc).toInstant().toEpochMilli()
|
showCustomMonthPicker = false
|
||||||
val ed = Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate().atStartOfDay(utc).toInstant().toEpochMilli() + 86400_000L - 1
|
}, { showCustomMonthPicker = false })
|
||||||
viewModel.setCustomRange(sd, ed); showCustomRangeEnd = false }, { showCustomRangeEnd = false })
|
if (showCustomRangeStart) DateOnlyPickerDialog("开始日期", { date ->
|
||||||
|
customRangeStartDate = date
|
||||||
|
showCustomRangeStart = false
|
||||||
|
showCustomRangeEnd = true
|
||||||
|
}, { showCustomRangeStart = false })
|
||||||
|
if (showCustomRangeEnd) {
|
||||||
|
val startDate = customRangeStartDate
|
||||||
|
DateOnlyPickerDialog(
|
||||||
|
title = "结束日期",
|
||||||
|
onConfirm = { endDate ->
|
||||||
|
if (startDate != null && !endDate.isBefore(startDate)) {
|
||||||
|
viewModel.setCustomRange(startDate, endDate)
|
||||||
|
showCustomRangeEnd = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismiss = { showCustomRangeEnd = false },
|
||||||
|
minDate = startDate
|
||||||
|
)
|
||||||
|
}
|
||||||
val models = state.selectedModels.ifEmpty { state.allModels.toSet() }
|
val models = state.selectedModels.ifEmpty { state.allModels.toSet() }
|
||||||
if (showCostDetail) ChartDetailDialog("消费明细", { showCostDetail = false }) {
|
if (showCostDetail) ChartDetailDialog("消费明细", { showCostDetail = false }) {
|
||||||
models.forEach { model -> val t = state.buckets.sumOf { it.byModel[model]?.cost ?:0L }; if (t>0) DetailRow(model, "$${String.format(Locale.US, "%.4f", t/100_000_000.0)}") }
|
models.forEach { model -> val t = state.buckets.sumOf { it.byModel[model]?.cost ?:0L }; if (t>0) DetailRow(model, "$${String.format(Locale.US, "%.4f", t/100_000_000.0)}") }
|
||||||
@ -423,6 +442,63 @@ internal fun CustomTimeRangeRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun DateOnlyPickerDialog(
|
||||||
|
title: String,
|
||||||
|
onConfirm: (LocalDate) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
minDate: LocalDate? = null
|
||||||
|
) {
|
||||||
|
val utc = remember { ZoneOffset.UTC }
|
||||||
|
val minDateStartMs = remember(minDate) { minDate?.atStartOfDay(utc)?.toInstant()?.toEpochMilli() }
|
||||||
|
val selectableDates = remember(minDateStartMs) {
|
||||||
|
object : SelectableDates {
|
||||||
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||||
|
return minDateStartMs == null || utcTimeMillis >= minDateStartMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val dateState = rememberDatePickerState(selectableDates = selectableDates)
|
||||||
|
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
val millis = dateState.selectedDateMillis ?: return@TextButton
|
||||||
|
onConfirm(Instant.ofEpochMilli(millis).atOffset(utc).toLocalDate())
|
||||||
|
},
|
||||||
|
enabled = dateState.selectedDateMillis != null
|
||||||
|
) {
|
||||||
|
Text("确定")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
if (minDate != null) {
|
||||||
|
Text(
|
||||||
|
text = "结束日期不能早于开始日期 ${minDate.formatDateLabel()}",
|
||||||
|
modifier = Modifier.padding(start = 24.dp, top = 8.dp, end = 24.dp),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = InkMuted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DatePicker(state = dateState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LocalDate.formatDateLabel(): String =
|
||||||
|
String.format(Locale.US, "%04d-%02d-%02d", year, monthValue, dayOfMonth)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DateTimePickerDialog(
|
internal fun DateTimePickerDialog(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user