Files
happy-life-star/EmotionMuseum/EmotionMuseum/Views/GrowthView.swift
T

808 lines
26 KiB
Swift

//
// GrowthView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
struct GrowthView: View {
@StateObject private var themeManager = ThemeManager()
@State private var showingTopicDetail = false
@State private var selectedTopic: GrowthTopic?
@State private var showingInsights = false
@State private var showingRadarChart = false
@State private var loadingState: LoadingState = .idle
@State private var isInitialLoading = true
var body: some View {
NavigationView {
LoadingStateView(loadingState: isInitialLoading ? .loading : .loaded) {
ScrollView {
VStack(spacing: 24) {
//
EmotionalInsightCard {
showingInsights = true
}
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
UserProfileRadarCard {
showingRadarChart = true
}
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
GrowthTopicsSection { topic in
selectedTopic = topic
showingTopicDetail = true
}
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
TodayActionCard()
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
GrowthTimelineCard()
.transition(.scale(scale: 0.8).combined(with: .opacity))
}
.padding(.horizontal)
.padding(.vertical)
}
.refreshable {
await refreshData()
}
} loadingView: {
AnyView(growthViewSkeleton)
}
.navigationTitle("成长治愈")
.navigationBarTitleDisplayMode(.large)
}
.environmentObject(themeManager)
.preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
.sheet(isPresented: $showingTopicDetail) {
if let topic = selectedTopic {
TopicDetailView(topic: topic)
.environmentObject(themeManager)
}
}
.sheet(isPresented: $showingInsights) {
EmotionalInsightsView()
.environmentObject(themeManager)
}
.sheet(isPresented: $showingRadarChart) {
NavigationView {
VStack {
Text("用户画像雷达图")
.font(.title)
.padding()
Text("这里显示用户的多维度能力雷达图")
.foregroundColor(.secondary)
.padding()
Spacer()
}
.navigationTitle("用户画像")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("关闭") {
showingRadarChart = false
}
}
}
}
.environmentObject(themeManager)
}
.onAppear {
simulateInitialLoading()
}
}
// MARK: -
private func simulateInitialLoading() {
loadingState = .loading
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
withAnimation(.easeOut(duration: 0.8)) {
isInitialLoading = false
loadingState = .loaded
}
}
}
private func refreshData() async {
loadingState = .loading
//
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8
DispatchQueue.main.async {
withAnimation(.easeOut(duration: 0.6)) {
loadingState = .loaded
}
}
}
// MARK: -
private var growthViewSkeleton: some View {
ScrollView {
VStack(spacing: 24) {
//
insightCardSkeleton
//
radarCardSkeleton
//
topicsGridSkeleton
//
actionCardSkeleton
//
timelineCardSkeleton
}
.padding(.horizontal)
.padding(.vertical)
}
.background(Color.theme.background)
}
private var insightCardSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 4) {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
SkeletonView(width: 140, height: 12, cornerRadius: 3)
}
Spacer()
SkeletonView(width: 24, height: 24, cornerRadius: 12)
}
VStack(spacing: 12) {
ForEach(0..<3, id: \.self) { _ in
HStack(spacing: 12) {
SkeletonView(width: 20, height: 16, cornerRadius: 4)
SkeletonView(width: 80, height: 14, cornerRadius: 3)
Spacer()
SkeletonView(width: 60, height: 14, cornerRadius: 3)
}
}
}
HStack {
SkeletonView(width: 100, height: 12, cornerRadius: 3)
Spacer()
SkeletonView(width: 12, height: 12, cornerRadius: 3)
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
private var radarCardSkeleton: some View {
VStack(spacing: 16) {
HStack {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 24, height: 24, cornerRadius: 12)
}
VStack(spacing: 8) {
ForEach(0..<5, id: \.self) { _ in
HStack {
SkeletonView(width: 60, height: 12, cornerRadius: 3)
SkeletonView(height: 8, cornerRadius: 4)
SkeletonView(width: 40, height: 12, cornerRadius: 3)
}
}
}
HStack {
SkeletonView(width: 120, height: 12, cornerRadius: 3)
Spacer()
SkeletonView(width: 12, height: 12, cornerRadius: 3)
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
private var topicsGridSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 60, height: 12, cornerRadius: 3)
}
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
ForEach(0..<4, id: \.self) { _ in
VStack(alignment: .leading, spacing: 12) {
HStack {
SkeletonView(width: 20, height: 20, cornerRadius: 10)
Spacer()
SkeletonView(width: 30, height: 12, cornerRadius: 3)
}
VStack(alignment: .leading, spacing: 4) {
SkeletonView(width: 100, height: 14, cornerRadius: 3)
SkeletonView(width: 120, height: 12, cornerRadius: 3)
SkeletonView(width: 80, height: 12, cornerRadius: 3)
}
VStack(spacing: 4) {
HStack {
SkeletonView(width: 30, height: 10, cornerRadius: 2)
Spacer()
SkeletonView(width: 30, height: 10, cornerRadius: 2)
}
SkeletonView(height: 4, cornerRadius: 2)
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(12)
}
}
}
}
private var actionCardSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 24, height: 24, cornerRadius: 12)
}
VStack(spacing: 12) {
ForEach(0..<3, id: \.self) { _ in
HStack(spacing: 12) {
SkeletonView(width: 24, height: 24, cornerRadius: 12)
VStack(alignment: .leading, spacing: 2) {
SkeletonView(width: 160, height: 14, cornerRadius: 3)
HStack(spacing: 8) {
SkeletonView(width: 60, height: 16, cornerRadius: 8)
SkeletonView(width: 40, height: 12, cornerRadius: 3)
}
}
Spacer()
}
}
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
private var timelineCardSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
VStack(alignment: .leading, spacing: 12) {
ForEach(0..<4, id: \.self) { _ in
HStack(spacing: 12) {
VStack {
SkeletonView(width: 8, height: 8, cornerRadius: 4)
SkeletonView(width: 1, height: 20, cornerRadius: 1)
}
VStack(alignment: .leading, spacing: 2) {
HStack {
SkeletonView(width: 100, height: 14, cornerRadius: 3)
Spacer()
SkeletonView(width: 40, height: 12, cornerRadius: 3)
}
SkeletonView(width: 180, height: 12, cornerRadius: 3)
}
}
}
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
}
// MARK: -
struct EmotionalInsightCard: View {
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(alignment: .leading, spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("情绪洞察")
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(Color.theme.primaryText)
Text("基于你的对话记录生成")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
}
Spacer()
Image(systemName: "brain.head.profile")
.font(.title2)
.foregroundColor(.purple)
}
VStack(alignment: .leading, spacing: 12) {
InsightRow(
icon: "heart.fill",
title: "主要情绪状态",
value: "平静自省",
color: .blue
)
InsightRow(
icon: "target",
title: "成长焦点",
value: "人际关系",
color: .green
)
InsightRow(
icon: "chart.line.uptrend.xyaxis",
title: "进步指数",
value: "↗️ 稳步提升",
color: .orange
)
}
HStack {
Text("点击查看详细分析")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
.buttonStyle(PlainButtonStyle())
}
}
struct InsightRow: View {
let icon: String
let title: String
let value: String
let color: Color
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.subheadline)
.foregroundColor(color)
.frame(width: 20)
Text(title)
.font(.subheadline)
.foregroundColor(Color.theme.secondaryText)
Spacer()
Text(value)
.font(.subheadline)
.fontWeight(.medium)
}
}
}
// MARK: -
struct UserProfileRadarCard: View {
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(spacing: 16) {
HStack {
Text("成长画像")
.font(.headline)
.fontWeight(.semibold)
Spacer()
Image(systemName: "person.crop.circle.badge.checkmark")
.font(.title2)
.foregroundColor(.green)
}
//
VStack(spacing: 8) {
ProfileDimensionRow(name: "自我感知", value: 0.8, color: .blue)
ProfileDimensionRow(name: "情绪韧性", value: 0.7, color: .purple)
ProfileDimensionRow(name: "行动力", value: 0.6, color: .orange)
ProfileDimensionRow(name: "共情力", value: 0.9, color: .green)
ProfileDimensionRow(name: "生活热度", value: 0.7, color: .red)
}
HStack {
Text("点击查看完整雷达图")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
.buttonStyle(PlainButtonStyle())
}
}
struct ProfileDimensionRow: View {
let name: String
let value: Double
let color: Color
var body: some View {
HStack {
Text(name)
.font(.caption)
.foregroundColor(.primary)
.frame(width: 60, alignment: .leading)
ProgressView(value: value)
.progressViewStyle(LinearProgressViewStyle(tint: color))
Text("\(Int(value * 100))%")
.font(.caption)
.fontWeight(.medium)
.foregroundColor(color)
.frame(width: 40, alignment: .trailing)
}
}
}
// MARK: -
struct GrowthTopicsSection: View {
let onTopicTap: (GrowthTopic) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("成长课题")
.font(.headline)
.fontWeight(.semibold)
Spacer()
Text("3个进行中")
.font(.caption)
.foregroundColor(.secondary)
}
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
ForEach(sampleGrowthTopics) { topic in
TopicCard(topic: topic) {
onTopicTap(topic)
}
}
}
}
}
}
struct TopicCard: View {
let topic: GrowthTopic
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: topic.icon)
.font(.title3)
.foregroundColor(topic.color)
Spacer()
Text("Lv.\(topic.level)")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(topic.color)
}
VStack(alignment: .leading, spacing: 4) {
Text(topic.title)
.font(.subheadline)
.fontWeight(.medium)
.multilineTextAlignment(.leading)
Text(topic.description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
}
VStack(spacing: 4) {
HStack {
Text("进度")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text("\(Int(topic.progress * 100))%")
.font(.caption2)
.fontWeight(.medium)
}
ProgressView(value: topic.progress)
.progressViewStyle(LinearProgressViewStyle(tint: topic.color))
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemGray6))
)
}
.buttonStyle(PlainButtonStyle())
}
}
// MARK: -
struct TodayActionCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("今日推荐")
.font(.headline)
.fontWeight(.semibold)
Spacer()
Image(systemName: "lightbulb.fill")
.font(.title3)
.foregroundColor(.yellow)
}
VStack(spacing: 12) {
ActionItemView(
title: "与朋友分享一件开心的事",
category: "人际关系",
duration: "5分钟",
color: .blue
)
ActionItemView(
title: "写下今天的三个感恩点",
category: "自我觉察",
duration: "10分钟",
color: .green
)
ActionItemView(
title: "深呼吸练习",
category: "情绪调节",
duration: "3分钟",
color: .purple
)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
}
struct ActionItemView: View {
let title: String
let category: String
let duration: String
let color: Color
@State private var isCompleted = false
var body: some View {
HStack(spacing: 12) {
Button(action: { isCompleted.toggle() }) {
Image(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundColor(isCompleted ? color : .gray)
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
.strikethrough(isCompleted)
HStack(spacing: 8) {
Text(category)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(color.opacity(0.2))
.foregroundColor(color)
.cornerRadius(8)
Text(duration)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
.opacity(isCompleted ? 0.6 : 1.0)
.animation(.easeInOut(duration: 0.2), value: isCompleted)
}
}
// MARK: -
struct GrowthTimelineCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("成长历程")
.font(.headline)
.fontWeight(.semibold)
VStack(alignment: .leading, spacing: 12) {
TimelineItem(
date: "今天",
title: "完成情绪记录",
description: "记录了平静的心情状态",
color: .blue,
isRecent: true
)
TimelineItem(
date: "昨天",
title: "课题升级",
description: "人际关系课题提升到Lv.2",
color: .green,
isRecent: true
)
TimelineItem(
date: "3天前",
title: "解锁新课题",
description: "开始学习情绪调节技巧",
color: .purple,
isRecent: false
)
TimelineItem(
date: "1周前",
title: "达成里程碑",
description: "连续7天完成情绪记录",
color: .orange,
isRecent: false
)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
}
struct TimelineItem: View {
let date: String
let title: String
let description: String
let color: Color
let isRecent: Bool
var body: some View {
HStack(spacing: 12) {
VStack {
Circle()
.fill(color)
.frame(width: 8, height: 8)
if !isRecent {
Rectangle()
.fill(Color(.systemGray4))
.frame(width: 1, height: 20)
}
}
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
Spacer()
Text(date)
.font(.caption)
.foregroundColor(.secondary)
}
Text(description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
.opacity(isRecent ? 1.0 : 0.7)
}
}
// MARK: - GrowthTopic UI
extension GrowthTopic {
var icon: String {
category.icon
}
var color: Color {
category.color
}
}
// MARK: -
let sampleGrowthTopics = [
GrowthTopic(
title: "人际关系边界",
description: "学习建立健康的人际关系边界",
category: .relationships,
difficulty: .intermediate,
progress: 0.7,
level: 2
),
GrowthTopic(
title: "情绪调节技能",
description: "掌握情绪识别和调节的核心技能",
category: .emotionRegulation,
difficulty: .beginner,
progress: 0.3,
level: 1
),
GrowthTopic(
title: "深度自我认知",
description: "提升对内在世界的认知和理解",
category: .selfAwareness,
difficulty: .advanced,
progress: 0.9,
level: 3
),
GrowthTopic(
title: "行动力提升",
description: "培养执行力和目标达成能力",
category: .lifeGoals,
difficulty: .beginner,
progress: 0.4,
level: 1
)
]