fix: 修复自定义日期时区并统一用量入口
This commit is contained in:
parent
1d98d4dee1
commit
45c8f71b8a
@ -299,7 +299,11 @@ private fun rememberDashboardItems(
|
||||
DashboardCard(
|
||||
card = card,
|
||||
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
|
||||
private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit, onOpenCcgoUsageDetail: (() -> Unit)? = null) {
|
||||
private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit, onOpenUsageDetail: (() -> Unit)? = null) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@ -578,10 +582,10 @@ private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit, onOpenCcgo
|
||||
// ─── 主体:服务特定的主信息 ───
|
||||
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))
|
||||
TextButton(onClick = onOpenCcgoUsageDetail) {
|
||||
TextButton(onClick = onOpenUsageDetail) {
|
||||
Text("查看用量详情", color = StrawberryPink)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
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.StrawberryPink
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@ -103,7 +106,7 @@ fun UsageChartScreen(
|
||||
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
||||
var showCustomRangeStart 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 showReqDetail by remember { mutableStateOf(false) }
|
||||
var showTokenDetail by remember { mutableStateOf(false) }
|
||||
@ -364,10 +367,7 @@ fun UsageChartScreen(
|
||||
DateTimePickerDialog(
|
||||
title = "选择日期",
|
||||
onConfirm = { ms ->
|
||||
val utc = java.time.ZoneOffset.UTC
|
||||
val dayStart = java.time.Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate()
|
||||
.atStartOfDay(utc).toInstant().toEpochMilli()
|
||||
viewModel.setCustomDayRange(dayStart, dayStart + 86400_000L - 1)
|
||||
viewModel.setCustomDay(ms.toUtcLocalDate())
|
||||
showCustomDayPicker = false
|
||||
},
|
||||
onDismiss = { showCustomDayPicker = false }
|
||||
@ -377,7 +377,7 @@ fun UsageChartScreen(
|
||||
DateTimePickerDialog(
|
||||
title = "选择月份(任意一天)",
|
||||
onConfirm = { ms ->
|
||||
viewModel.setCustomMonth(ms)
|
||||
viewModel.setCustomMonth(ms.toUtcLocalDate())
|
||||
showCustomMonthPicker = false
|
||||
},
|
||||
onDismiss = { showCustomMonthPicker = false }
|
||||
@ -387,7 +387,7 @@ fun UsageChartScreen(
|
||||
DateTimePickerDialog(
|
||||
title = "开始日期",
|
||||
onConfirm = { ms ->
|
||||
customRangeStartMs = ms
|
||||
customRangeStartDate = ms.toUtcLocalDate()
|
||||
showCustomRangeStart = false
|
||||
showCustomRangeEnd = true
|
||||
},
|
||||
@ -398,13 +398,12 @@ fun UsageChartScreen(
|
||||
DateTimePickerDialog(
|
||||
title = "结束日期",
|
||||
onConfirm = { ms ->
|
||||
val utc = java.time.ZoneOffset.UTC
|
||||
val startDay = java.time.Instant.ofEpochMilli(customRangeStartMs).atOffset(utc).toLocalDate()
|
||||
.atStartOfDay(utc).toInstant().toEpochMilli()
|
||||
val endDay = java.time.Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate()
|
||||
.atStartOfDay(utc).toInstant().toEpochMilli() + 86400_000L - 1
|
||||
viewModel.setCustomRange(startDay, endDay)
|
||||
showCustomRangeEnd = false
|
||||
val startDate = customRangeStartDate ?: return@DateTimePickerDialog
|
||||
val endDate = ms.toUtcLocalDate()
|
||||
if (!endDate.isBefore(startDate)) {
|
||||
viewModel.setCustomRange(startDate, endDate)
|
||||
showCustomRangeEnd = false
|
||||
}
|
||||
},
|
||||
onDismiss = { showCustomRangeEnd = false }
|
||||
)
|
||||
@ -486,6 +485,9 @@ fun UsageChartScreen(
|
||||
// 图表卡片容器
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
private fun Long.toUtcLocalDate(): LocalDate =
|
||||
Instant.ofEpochMilli(this).atOffset(ZoneOffset.UTC).toLocalDate()
|
||||
|
||||
@Composable
|
||||
internal fun ChartCard(
|
||||
title: String,
|
||||
|
||||
@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
@ -157,33 +158,32 @@ class UsageChartViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCustomDayRange(from: Long, to: Long) {
|
||||
fun setCustomDay(date: LocalDate) {
|
||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_DAY_HOURLY) }
|
||||
_customDayRange = from to to
|
||||
_customDay = date
|
||||
load()
|
||||
}
|
||||
|
||||
fun setCustomMonth(monthStartMs: Long) {
|
||||
fun setCustomMonth(monthDate: LocalDate) {
|
||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_MONTH_DAILY) }
|
||||
_customMonthStart = monthStartMs
|
||||
_customMonth = monthDate.withDayOfMonth(1)
|
||||
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) }
|
||||
_customRange = from to to
|
||||
_customRange = fromDate to toDate
|
||||
load()
|
||||
}
|
||||
|
||||
private var _customDayRange: Pair<Long, Long> = 0L to 0L
|
||||
private var _customMonthStart: Long = 0L
|
||||
private var _customRange: Pair<Long, Long> = 0L to 0L
|
||||
private var _customDay: LocalDate? = null
|
||||
private var _customMonth: LocalDate? = null
|
||||
private var _customRange: Pair<LocalDate, LocalDate>? = null
|
||||
|
||||
private fun timeRangeFor(g: ChartGranularity): Pair<Long?, Long?> {
|
||||
val now = System.currentTimeMillis()
|
||||
val useUtc8 = _state.value.useUtc8
|
||||
val zoneOffset = if (useUtc8) ZoneOffset.ofHours(8) else ZoneOffset.UTC
|
||||
val zoneOffset = if (_state.value.useUtc8) ZoneOffset.ofHours(8) else ZoneOffset.UTC
|
||||
val today = Instant.ofEpochMilli(now).atOffset(zoneOffset).toLocalDate()
|
||||
val todayMidnight = today.atStartOfDay(zoneOffset).toInstant().toEpochMilli()
|
||||
return when (g) {
|
||||
@ -192,37 +192,36 @@ class UsageChartViewModel @Inject constructor(
|
||||
ChartGranularity.LAST_24H_HOURLY -> now - 24 * 3600_000L to null
|
||||
ChartGranularity.TODAY_HOURLY -> todayMidnight to todayMidnight + 86400_000L - 1
|
||||
ChartGranularity.YESTERDAY_HOURLY -> todayMidnight - 86400_000L to todayMidnight - 1
|
||||
ChartGranularity.LAST_7D_DAILY -> {
|
||||
todayMidnight - 6 * 86400_000L to todayMidnight + 86400_000L - 1
|
||||
}
|
||||
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.LAST_7D_DAILY -> 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_MONTH_DAILY -> {
|
||||
if (_customMonthStart == 0L) null to null
|
||||
else {
|
||||
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
|
||||
}
|
||||
val month = _customMonth ?: return null to null
|
||||
monthRange(month, zoneOffset)
|
||||
}
|
||||
ChartGranularity.CUSTOM_RANGE_DAILY -> {
|
||||
if (_customRange.first == 0L) null to null
|
||||
else _customRange.first to _customRange.second
|
||||
val range = _customRange ?: return null to null
|
||||
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(
|
||||
buckets: List<ChartBucket>,
|
||||
granularity: ChartGranularity,
|
||||
|
||||
@ -29,6 +29,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SelectableDates
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@ -93,7 +94,7 @@ fun UsageDetailScreen(
|
||||
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
||||
var showCustomRangeStart 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 showReqDetail by remember { mutableStateOf(false) }
|
||||
var showTokenDetail by remember { mutableStateOf(false) }
|
||||
@ -299,15 +300,33 @@ fun UsageDetailScreen(
|
||||
}
|
||||
}
|
||||
// 自定义日期选择器
|
||||
if (showCustomDayPicker) DateTimePickerDialog("选择日期", { ms ->
|
||||
val utc = ZoneOffset.UTC; val ds = Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate().atStartOfDay(utc).toInstant().toEpochMilli()
|
||||
viewModel.setCustomDayRange(ds, ds + 86400_000L - 1); showCustomDayPicker = false }, { showCustomDayPicker = false })
|
||||
if (showCustomMonthPicker) DateTimePickerDialog("选择月份(任意一天)", { viewModel.setCustomMonth(it); showCustomMonthPicker = false }, { showCustomMonthPicker = false })
|
||||
if (showCustomRangeStart) DateTimePickerDialog("开始日期", { customRangeStartMs = it; showCustomRangeStart = false; showCustomRangeEnd = true }, { showCustomRangeStart = false })
|
||||
if (showCustomRangeEnd) DateTimePickerDialog("结束日期", { ms ->
|
||||
val utc = ZoneOffset.UTC; val sd = Instant.ofEpochMilli(customRangeStartMs).atOffset(utc).toLocalDate().atStartOfDay(utc).toInstant().toEpochMilli()
|
||||
val ed = Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate().atStartOfDay(utc).toInstant().toEpochMilli() + 86400_000L - 1
|
||||
viewModel.setCustomRange(sd, ed); showCustomRangeEnd = false }, { showCustomRangeEnd = false })
|
||||
if (showCustomDayPicker) DateOnlyPickerDialog("选择日期", { date ->
|
||||
viewModel.setCustomDay(date)
|
||||
showCustomDayPicker = false
|
||||
}, { showCustomDayPicker = false })
|
||||
if (showCustomMonthPicker) DateOnlyPickerDialog("选择月份(任意一天)", { date ->
|
||||
viewModel.setCustomMonth(date)
|
||||
showCustomMonthPicker = false
|
||||
}, { showCustomMonthPicker = 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() }
|
||||
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)}") }
|
||||
@ -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)
|
||||
@Composable
|
||||
internal fun DateTimePickerDialog(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user