import SwiftUI /// 首页 - 对应 Android 版 HomeActivity + UserFragment struct HomeView: View { @StateObject private var viewModel = HomeViewModel() @EnvironmentObject var appState: AppState @State private var categoryScrollAnchorId: String? // 网格布局 #if os(iOS) private let columns = [ GridItem(.adaptive(minimum: 100, maximum: 140), spacing: 10) ] #else private let columns = [ GridItem(.adaptive(minimum: 140, maximum: 180), spacing: 16) ] #endif var body: some View { NavigationStack { VStack(spacing: 0) { // 顶部栏 headerBar // 分类标签栏 if !viewModel.sorts.isEmpty { categoryTabBar } // 内容区 contentArea } .background(AppTheme.primaryGradient) } .task { await viewModel.loadSorts() if let first = viewModel.sorts.first { viewModel.selectSort(first) } } } // MARK: - 顶部栏(源选择器) private var headerBar: some View { HStack(spacing: 12) { // 源切换按钮 Menu { ForEach(ApiConfig.shared.sourceBeanList.filter { $0.isSupportedInSwift }) { source in Button { ApiConfig.shared.setHomeSource(source) Task { await viewModel.refresh() } } label: { HStack { Text(source.name) if source.key == ApiConfig.shared.homeSourceBean?.key { Image(systemName: "checkmark") } } } } } label: { HStack(spacing: 6) { Image(systemName: "play.tv.fill") .font(.system(size: 14)) .foregroundColor(.orange) Text(ApiConfig.shared.homeSourceBean?.name ?? "TVBox") .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) .lineLimit(1) Image(systemName: "chevron.down") .font(.system(size: 10, weight: .semibold)) .foregroundColor(.white.opacity(0.5)) } } .menuStyle(.borderlessButton) .fixedSize() Spacer() } .padding(.horizontal, 16) .padding(.top, 10) .padding(.bottom, 4) } // MARK: - 分类标签栏 private var categoryTabBar: some View { ScrollViewReader { proxy in ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 0) { ForEach(viewModel.sorts) { sort in Button { withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { viewModel.selectSort(sort) } categoryScrollAnchorId = sort.id scrollCategoryBar(to: sort.id, proxy: proxy) } label: { VStack(spacing: 6) { Text(sort.name) .font(.system(size: 14, weight: viewModel.selectedSort?.id == sort.id ? .bold : .regular)) .foregroundColor(viewModel.selectedSort?.id == sort.id ? .white : .white.opacity(0.6)) // 底部指示条 RoundedRectangle(cornerRadius: 1.5) .fill(Color.orange) .frame(width: 20, height: 3) .opacity(viewModel.selectedSort?.id == sort.id ? 1 : 0) } .padding(.horizontal, 14) .padding(.vertical, 8) } .buttonStyle(.plain) .id(sort.id) } } .padding(.horizontal, 12) } .onAppear { syncCategoryScrollAnchorIfNeeded() scrollCategoryBar(to: categoryScrollAnchorId, proxy: proxy, animated: false) } .onChange(of: viewModel.sorts.map(\.id)) { oldValue, newValue in syncCategoryScrollAnchorIfNeeded() scrollCategoryBar(to: categoryScrollAnchorId, proxy: proxy, animated: false) } .onChange(of: viewModel.selectedSort?.id) { oldId, newId in guard let newId else { return } categoryScrollAnchorId = newId scrollCategoryBar(to: newId, proxy: proxy) } } .padding(.bottom, 4) } private func categoryIndex(for id: String?) -> Int? { guard let id else { return nil } return viewModel.sorts.firstIndex(where: { $0.id == id }) } private func syncCategoryScrollAnchorIfNeeded() { guard !viewModel.sorts.isEmpty else { categoryScrollAnchorId = nil return } if let selectedId = viewModel.selectedSort?.id, viewModel.sorts.contains(where: { $0.id == selectedId }) { categoryScrollAnchorId = selectedId return } if let anchorId = categoryScrollAnchorId, viewModel.sorts.contains(where: { $0.id == anchorId }) { return } categoryScrollAnchorId = viewModel.sorts.first?.id } private func scrollCategoryBar(to id: String?, proxy: ScrollViewProxy, animated: Bool = true) { guard let id else { return } if animated { withAnimation(.easeInOut(duration: 0.2)) { proxy.scrollTo(id, anchor: .center) } } else { proxy.scrollTo(id, anchor: .center) } } // MARK: - 内容区 private var contentArea: some View { Group { if viewModel.isLoading && viewModel.categoryVideos.isEmpty && viewModel.homeVideos.isEmpty { VStack { Spacer() ProgressView() .scaleEffect(1.5) .tint(.orange) Text("加载中...") .font(.subheadline) .foregroundColor(.secondary) .padding(.top, 12) Spacer() } } else if let error = viewModel.errorMessage, viewModel.categoryVideos.isEmpty && viewModel.homeVideos.isEmpty { VStack(spacing: 12) { Spacer() Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text(error) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 40) // 如果是不支持的源类型,显示类型信息 if let source = ApiConfig.shared.homeSourceBean, !source.isSupportedInSwift { Text("当前源类型: \(source.typeDescription)") .font(.caption) .foregroundColor(.secondary) } Button("重试") { Task { await viewModel.refresh() } } .buttonStyle(.borderedProminent) .tint(.orange) Spacer() } } else { let videos = viewModel.selectedSort?.id == "home" ? viewModel.homeVideos : viewModel.categoryVideos ScrollView { LazyVGrid(columns: columns, spacing: 16) { ForEach(videos) { video in NavigationLink(value: video) { VodCardView(video: video) } #if os(iOS) .buttonStyle(VodCardPressStyle()) #else .buttonStyle(.plain) #endif .onAppear { Task { await viewModel.loadMoreIfNeeded(currentItem: video) } } } } .padding(.horizontal, 20) .padding(.vertical, 12) // 加载更多 if viewModel.selectedSort?.id != "home" && viewModel.hasMore { ProgressView() .padding() } } .refreshable { await viewModel.refresh() } } } .navigationDestination(for: Movie.Video.self) { video in DetailView(video: video) } } }