import SwiftUI import Combine import Foundation import UIKit struct PhotoView: View { let albumId: String @StateObject private var viewModel: PhotoViewModel @State private var currentImageIndex: Int = 0 // 当前可见的图片索引 @State private var progress: Double = 0 // 进度条比例(0-1) @State private var totalImages: Int = 0 // 总图片数量 @State private var isFullscreen = false // 全屏状态标记 @Environment(\.presentationMode) var presentationMode // 返回上级页面 @EnvironmentObject var tabState: TabBarState // 获取状态对象 // 定时器:降低保存频率,减少性能消耗 private let saveTimer = Timer.publish(every: 2, on: .main, in: .common) private var timerCancellable: AnyCancellable? // 用于存储定时器订阅 init(albumId: String) { self.albumId = albumId self._viewModel = StateObject(wrappedValue: PhotoViewModel(albumId: albumId)) self.timerCancellable = saveTimer.autoconnect().sink(receiveValue: { _ in }) } var body: some View { ZStack(alignment: .bottom) { // 背景色(根据全屏状态切换) Color(isFullscreen ? .black : .systemBackground) .edgesIgnoringSafeArea(.all) if viewModel.isLoading { ProgressView("加载中...") .foregroundColor(isFullscreen ? .white : .black) } else if let error = viewModel.error { VStack(spacing: 16) { Text("加载失败") .font(.headline) .foregroundColor(isFullscreen ? .white : .black) Text(error.localizedDescription) .font(.subheadline) .foregroundColor(isFullscreen ? .gray : .secondary) Button("重试") { viewModel.loadAlbumData() } .foregroundColor(.blue) .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(8) } } else if let album = viewModel.album { ScrollView { ScrollViewReader { proxy in // 漫画信息头部(全屏时隐藏) if !isFullscreen { VStack(alignment: .leading, spacing: 8) { Text(album.name) .font(.headline) Text("作者: \(album.authors.joined(separator: ", "))") .font(.subheadline) .foregroundColor(isFullscreen ? .gray : .secondary) Text("标签: \(album.tags.joined(separator: ", "))") .font(.caption) .foregroundColor(isFullscreen ? .gray : .secondary) // 收藏按钮 Button(action: toggleFavorite) { Image(systemName: viewModel.isFavorite ? "heart.fill" : "heart") .foregroundColor(viewModel.isFavorite ? .red : .gray) Text(viewModel.isFavorite ? "取消收藏" : "收藏") .foregroundColor(isFullscreen ? .white : .black) } .padding(.vertical, 8) } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(isFullscreen ? Color.black : Color(.systemBackground)) .id("header") } // 图片列表 LazyVStack(spacing: 0) { ForEach(Array(album.image_urls.enumerated()), id: \.offset) { index, url in AsyncImageView( url: url, nums: album.nums, index: index ) .frame(maxWidth: .infinity) .id(index) // 当图片进入屏幕时更新当前索引 .onAppear { // 只有当图片索引大于当前索引时才更新(避免回滚时误判) if index > currentImageIndex || abs(index - currentImageIndex) > 3 { currentImageIndex = index updateProgressRatio() } } } } .onAppear { totalImages = album.image_urls.count // 初始化:加载上次保存的图片索引 let savedIndex = ReadingProgressManager.shared.getProgress(albumId: albumId) currentImageIndex = clamp(value: savedIndex, min: 0, max: totalImages - 1) updateProgressRatio() // 在PhotoView的onAppear中添加 UserDefaults.standard.addHistory(albumId: albumId) // 自动跳转到上次位置 print("上次阅读位置: \(savedIndex)") if totalImages > 0, currentImageIndex > 0 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { proxy.scrollTo(savedIndex, anchor: .top) } } } } // 进度条拖动时同步到对应图片 .onChange(of: progress) { newValue in let targetIndex = Int(newValue * Double(max(0, totalImages - 1))) let clampedIndex = clamp(value: targetIndex, min: 0, max: totalImages - 1) if clampedIndex != currentImageIndex { currentImageIndex = clampedIndex withAnimation { proxy.scrollTo(currentImageIndex, anchor: .top) } } } } } .padding(.top, isFullscreen ? 0 : 60) .edgesIgnoringSafeArea(isFullscreen ? .all : .vertical) .onTapGesture { // 点击切换全屏状态 withAnimation { tabState.toggleTabBar() isFullscreen.toggle() adjustSystemUI() } } // 定期保存进度 .onReceive(saveTimer) { _ in guard totalImages > 0 else { return } ReadingProgressManager.shared.saveProgress(albumId: albumId, imageIndex: currentImageIndex) } .onDisappear { ReadingProgressManager.shared.saveProgress(albumId: albumId, imageIndex: currentImageIndex) timerCancellable?.cancel() // 取消定时器订阅 // 恢复系统UI UIApplication.shared.isStatusBarHidden = false } // 控制栏(全屏时显示) if isFullscreen { // 顶部控制栏 VStack { HStack { // 返回按钮 Button(action: { presentationMode.wrappedValue.dismiss() }) { Image(systemName: "chevron.left") .foregroundColor(.white) .padding() .background(Color.black.opacity(0.5)) .cornerRadius(8) } Spacer() } .padding() Spacer() } } if totalImages > 0 && !isFullscreen { CustomSlider( value: $progress, trackHeight: 20, // 这里可以设置足够粗的高度(如20px) tintColor: .blue ) .background(Color(.systemBackground).opacity(0.9)) .cornerRadius(8) // 圆角配合高度,更美观 .shadow(radius: 5) .padding(.horizontal, 30) // 左右留空 .padding(.bottom, tabState.isTabBarHidden ? 30 : 60) // 底部距离 .padding(.top, 10) // 向上偏移 } } } .alert(isPresented: $viewModel.showAlert) { Alert( title: Text(viewModel.alertTitle), message: Text(viewModel.alertMessage), dismissButton: .default(Text("确定")) ) } .navigationBarHidden(isFullscreen) .statusBarHidden(isFullscreen) .onAppear { // 初始化系统UI状态 adjustSystemUI() } .onDisappear { // 离开阅读器时显示底部导航栏 tabState.showTabBar() } } // 调整系统UI(状态栏和导航栏) private func adjustSystemUI() { UIApplication.shared.isStatusBarHidden = isFullscreen // 强制刷新状态栏 UIApplication.shared.windows.first?.rootViewController?.setNeedsStatusBarAppearanceUpdate() } // 更新进度条比例 private func updateProgressRatio() { guard totalImages > 0 else { progress = 0 return } let safeTotal = max(1, totalImages - 1) // 避免除以0 progress = Double(currentImageIndex) / Double(safeTotal) } // 切换收藏状态 private func toggleFavorite() { FavoriteManager.shared.toggleFavorite(albumId: albumId) viewModel.isFavorite = FavoriteManager.shared.isFavorite(albumId: albumId) } // 限制数值范围 private func clamp(value: Int, min: Int, max: Int) -> Int { return Swift.max(min, Swift.min(value, max)) } } // 滚动偏移量偏好键 struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } class PhotoViewModel: ObservableObject { let albumId: String @Published var album: Album? @Published var isLoading = false @Published var error: Error? @Published var isFavorite: Bool = false // 收藏状态 @Published var showAlert = false @Published var alertTitle = "" @Published var alertMessage = "" private var cancellables = Set() init(albumId: String) { self.albumId = albumId checkIfFavorite() loadAlbumData() } // 检查是否已收藏 private func checkIfFavorite() { isFavorite = FavoriteManager.shared.isFavorite(albumId: albumId) } func loadAlbumData() { isLoading = true error = nil album = nil // 重置之前的数据 let urlString = Config.shared.apiURL(path: "album/\(albumId)/") guard let url = URL(string: urlString) else { let error = NSError(domain: "PhotoViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: "无效的URL"]) handleError(error: error) return } print("正在加载相册数据: \(urlString)") URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .decode(type: Album.self, decoder: JSONDecoder()) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { self?.handleError(error: error) } }, receiveValue: { [weak self] album in self?.isLoading = false self?.album = album print("成功加载相册: \(album.name), 包含 \(album.image_urls.count) 张图片") }) .store(in: &cancellables) } // 错误处理封装 private func handleError(error: Error) { self.error = error self.alertTitle = "加载失败" self.alertMessage = error.localizedDescription self.showAlert = true self.isLoading = false print("相册加载失败: \(error.localizedDescription)") } func showAlert(title: String, message: String) { alertTitle = title alertMessage = message showAlert = true } }