Files
legado-ios/App/Features/Activity/ReadingStatisticsView.swift
T
Aminark 45cf5b5482 fix(appearance): 完善深色模式和阅读页面对比度
- 区分按钮填充与图标颜色,统一书架、书源、搜索和阅读设置的选中态
- 调整编辑器语法高亮、键盘工具栏及统计卡片的浅深色显示
- 适配 RSS 正文和登录网页背景,保留书源自定义样式和页面状态
- 补充外观切换、页面对比度和管理页面的回归测试
2026-10-22 10:53:19 +08:00

178 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct ReadingStatisticsView: View {
@EnvironmentObject private var activity: ReadingActivityModel
@ScaledMetric(relativeTo: .largeTitle) private var durationFontSize = 42
@ScaledMetric(relativeTo: .caption2) private var chartHeight = 138
private var statistics: ReadingStatistics { activity.statistics }
var body: some View {
ScrollView {
VStack(spacing: 0) {
ReadingPageHeader(title: "阅读统计", subtitle: "每一页,都算数") { EmptyView() }
VStack(spacing: 16) {
todayCard
overviewGrid
weeklyCard
historyLink
}
.padding(.horizontal, 24)
.padding(.bottom, 32)
}
}
.background(ReadingTheme.canvas)
.navigationBarHidden(true)
}
private var todayCard: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
Text("今日阅读")
.font(.subheadline)
Spacer()
Text(Date.now.formatted(.dateTime.locale(Locale(identifier: "zh_CN")).month().day()))
.font(.caption)
}
.foregroundStyle(ReadingTheme.onAccent.opacity(0.7))
Text(duration(statistics.todayReadTime, compact: false))
.font(.system(size: durationFontSize, weight: .medium))
.foregroundStyle(ReadingTheme.onAccent)
.readingNumericTransition()
.minimumScaleFactor(0.7)
.lineLimit(1)
Text(statistics.todayReadTime == 0 ? "今天还没有阅读记录" : "已记录今天的阅读时长")
.font(.caption)
.foregroundStyle(ReadingTheme.onAccent.opacity(0.65))
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(24)
.background(ReadingTheme.emphasisSurface, in: RoundedRectangle(cornerRadius: 20))
.accessibilityElement(children: .contain)
.accessibilityIdentifier("statistics.todayCard")
}
private var overviewGrid: some View {
ReadingCard {
VStack(spacing: 18) {
HStack(alignment: .top, spacing: 20) {
statisticTile(title: "累计阅读", value: duration(statistics.totalReadTime, compact: true))
statisticTile(title: "阅读书籍", value: "\(statistics.bookCount) 本")
}
Rectangle().fill(ReadingTheme.separator).frame(height: 0.5)
HStack(alignment: .top, spacing: 20) {
statisticTile(
title: "近 7 天",
value: duration(statistics.recentDays.reduce(0) { $0 + $1.readTime }, compact: true)
)
statisticTile(title: "连续阅读", value: "\(statistics.currentStreak) 天")
}
}
}
}
private func statisticTile(title: String, value: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
Text(value)
.font(.title3.weight(.semibold))
.readingNumericTransition()
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(title),\(value)")
}
private var weeklyCard: some View {
ReadingCard {
VStack(alignment: .leading, spacing: 18) {
HStack {
Text("最近 7 天")
.font(.headline)
Spacer()
Text("每日阅读时长")
.font(.caption)
.foregroundStyle(.secondary)
}
HStack(alignment: .bottom, spacing: 10) {
ForEach(statistics.recentDays, id: \.date) { day in
VStack(spacing: 7) {
Text(barLabel(day.readTime))
.font(.caption2.monospacedDigit())
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.7)
RoundedRectangle(cornerRadius: 3)
.fill(day.readTime > 0 ? ReadingTheme.accent.opacity(Calendar.current.isDateInToday(day.date) ? 1 : 0.22) : ReadingTheme.separator)
.frame(width: 22, height: barHeight(day.readTime))
Text(weekday(day.date))
.font(.caption2)
.foregroundStyle(Calendar.current.isDateInToday(day.date) ? ReadingTheme.accent : .secondary)
}
.frame(maxWidth: .infinity)
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(weekday(day.date)),阅读 \(duration(day.readTime, compact: false))")
}
}
.frame(height: chartHeight, alignment: .bottom)
}
}
}
private var historyLink: some View {
NavigationLink {
ReadingHistoryView()
} label: {
ReadingCard {
HStack(spacing: 12) {
Image(systemName: "clock.arrow.circlepath").font(.title3)
Text("阅读记录").font(.subheadline.weight(.medium))
Spacer()
Image(systemName: "arrow.up.right").font(.subheadline)
}
.foregroundStyle(ReadingTheme.accent)
.frame(minHeight: 24)
.contentShape(Rectangle())
}
}
.buttonStyle(.plain)
}
private func barHeight(_ milliseconds: Int64) -> CGFloat {
let maximum = max(statistics.recentDays.map(\.readTime).max() ?? 0, 1)
guard milliseconds > 0 else { return 2 }
return 12 + 70 * CGFloat(milliseconds) / CGFloat(maximum)
}
private func barLabel(_ milliseconds: Int64) -> String {
guard milliseconds >= 60_000 else { return milliseconds > 0 ? "<1分" : "0" }
if milliseconds < 3_600_000 { return "\(milliseconds / 60_000)分" }
let hours = Double(milliseconds) / 3_600_000
return String(format: "%.1f时", hours)
}
private func weekday(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "EEE"
return formatter.string(from: date)
}
private func duration(_ milliseconds: Int64, compact: Bool) -> String {
let value = max(milliseconds, 0)
let hours = value / 3_600_000
let minutes = value % 3_600_000 / 60_000
if compact {
if hours > 0 { return minutes > 0 ? "\(hours)时\(minutes)分" : "\(hours)小时" }
return "\(minutes)分钟"
}
let seconds = value % 60_000 / 1_000
if hours > 0 { return minutes > 0 ? "\(hours)小时 \(minutes)分钟" : "\(hours)小时" }
if minutes > 0 { return "\(minutes)分钟" }
return "\(seconds)秒"
}
}