feat: 更新Splash视图及登录模型逻辑
- 将SplashV2替换为SplashPage,优化应用启动流程。 - 在LoginModels中将用户ID参数更改为加密后的ID,增强安全性。 - 更新AppConfig中的API基础URL,确保与生产环境一致。 - 在CommonComponents中添加底部Tab栏图标映射逻辑,提升用户体验。 - 新增NineGridImagePicker组件,支持多图选择功能,优化CreateFeedPage的图片选择逻辑。 - 移除冗余的BottomTabView组件,简化视图结构,提升代码整洁性。 - 在MainPage中整合创建动态页面的逻辑,优化导航体验。 - 新增MePage视图,提供用户信息管理功能,增强应用的可用性。
This commit is contained in:
@@ -392,7 +392,7 @@ struct LoginHelper {
|
||||
debugInfoSync(" 加密后密码: \(encryptedPassword)")
|
||||
|
||||
return IDLoginAPIRequest(
|
||||
phone: userID,
|
||||
phone: encryptedID,
|
||||
password: encryptedPassword
|
||||
)
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ struct AppConfig {
|
||||
static var baseURL: String {
|
||||
switch current {
|
||||
case .development:
|
||||
return "http://beta.api.molistar.xyz";//"http://beta.api.pekolive.com"
|
||||
return "http://beta.api.pekolive.com"
|
||||
case .production:
|
||||
return "https://api.epartylive.com"
|
||||
}
|
||||
|
@@ -25,29 +25,49 @@ struct BottomTabBar: View {
|
||||
var contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0)
|
||||
var horizontalPadding: CGFloat = 0
|
||||
|
||||
// 使用 BottomTabView.swift 中的图片资源名进行映射
|
||||
private func assetIconName(for item: TabBarItem, isSelected: Bool) -> String? {
|
||||
switch item.id {
|
||||
case "feed":
|
||||
return isSelected ? "feed selected" : "feed unselected"
|
||||
case "me":
|
||||
return isSelected ? "me selected" : "me unselected"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(items) { item in
|
||||
Button(action: {
|
||||
selectedId = item.id
|
||||
onSelect(item.id)
|
||||
}) {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: item.systemIconName)
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
|
||||
Text(item.title)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
|
||||
Group {
|
||||
if let name = assetIconName(for: item, isSelected: selectedId == item.id) {
|
||||
Image(name)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
} else {
|
||||
Image(systemName: item.systemIconName)
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(contentPadding)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8) // 按钮与边缘保持 8 间距
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.background(LiquidGlassBackground())
|
||||
.clipShape(Capsule())
|
||||
.contentShape(Capsule())
|
||||
.onTapGesture { /* 吸收空白区域点击,避免穿透 */ }
|
||||
.overlay(
|
||||
Capsule()
|
||||
.stroke(Color.white.opacity(0.12), lineWidth: 0.5)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
@MainActor
|
||||
final class CreateFeedViewModel: ObservableObject {
|
||||
@@ -11,19 +12,21 @@ final class CreateFeedViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
struct CreateFeedPage: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var viewModel = CreateFeedViewModel()
|
||||
let onDismiss: () -> Void
|
||||
|
||||
// MARK: - UI State
|
||||
@FocusState private var isTextEditorFocused: Bool
|
||||
@State private var isShowingSourceSheet: Bool = false
|
||||
@State private var isShowingImagePicker: Bool = false
|
||||
@State private var imagePickerSource: UIImagePickerController.SourceType = .photoLibrary
|
||||
@State private var isShowingPreview: Bool = false
|
||||
@State private var previewIndex: Int = 0
|
||||
|
||||
private let maxCharacters: Int = 500
|
||||
private let gridSpacing: CGFloat = 8
|
||||
private let gridCornerRadius: CGFloat = 16
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { _ in
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Color(hex: 0x0C0527)
|
||||
.ignoresSafeArea()
|
||||
@@ -33,13 +36,18 @@ struct CreateFeedPage: View {
|
||||
}
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
Button(action: onDismiss) {
|
||||
Button(action: {
|
||||
onDismiss()
|
||||
dismiss()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.frame(width: 44, height: 44, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
Spacer()
|
||||
Text(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||||
Text(LocalizedString ("createFeed.title", comment: "Image & Text Publish"))
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
Spacer()
|
||||
@@ -58,6 +66,8 @@ struct CreateFeedPage: View {
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
.contentShape(Rectangle())
|
||||
.zIndex(10)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A))
|
||||
@@ -74,6 +84,7 @@ struct CreateFeedPage: View {
|
||||
.scrollContentBackground(.hidden)
|
||||
.focused($isTextEditorFocused)
|
||||
.frame(height: 200)
|
||||
.zIndex(1) // 确保编辑器不会遮挡顶部栏的点击
|
||||
|
||||
// 字数统计(右下角)
|
||||
VStack { Spacer() }
|
||||
@@ -94,58 +105,17 @@ struct CreateFeedPage: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加图片按钮
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Button {
|
||||
isShowingSourceSheet = true
|
||||
} label: {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(Color(hex: 0x1C143A))
|
||||
.frame(width: 180, height: 180)
|
||||
Image(systemName: "plus")
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.font(.system(size: 36, weight: .semibold))
|
||||
}
|
||||
NineGridImagePicker(
|
||||
images: $viewModel.selectedImages,
|
||||
maxCount: 9,
|
||||
cornerRadius: gridCornerRadius,
|
||||
spacing: gridSpacing,
|
||||
horizontalPadding: 20,
|
||||
onTapImage: { index in
|
||||
previewIndex = index
|
||||
isShowingPreview = true
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// 已选图片预览(可滚动)
|
||||
if !viewModel.selectedImages.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(viewModel.selectedImages.indices, id: \.self) { index in
|
||||
Image(uiImage: viewModel.selectedImages[index])
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 180)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.confirmationDialog(LocalizedString("createFeed.chooseSource", comment: "Choose Source"), isPresented: $isShowingSourceSheet, titleVisibility: .visible) {
|
||||
Button(LocalizedString("createFeed.source.album", comment: "Photo Library")) {
|
||||
imagePickerSource = .photoLibrary
|
||||
isShowingImagePicker = true
|
||||
}
|
||||
Button(LocalizedString("createFeed.source.camera", comment: "Camera")) {
|
||||
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||
imagePickerSource = .camera
|
||||
isShowingImagePicker = true
|
||||
}
|
||||
}
|
||||
Button(LocalizedString("common.cancel", comment: "Cancel"), role: .cancel) {}
|
||||
}
|
||||
.sheet(isPresented: $isShowingImagePicker) {
|
||||
ImagePicker(sourceType: imagePickerSource) { image in
|
||||
viewModel.selectedImages.append(image)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if let error = viewModel.errorMessage {
|
||||
Text(error)
|
||||
@@ -158,53 +128,103 @@ struct CreateFeedPage: View {
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.fullScreenCover(isPresented: $isShowingPreview) {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
isShowingPreview = false
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.padding(12)
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
|
||||
TabView(selection: $previewIndex) {
|
||||
ForEach(viewModel.selectedImages.indices, id: \.self) { idx in
|
||||
ZStack {
|
||||
Color.black
|
||||
Image(uiImage: viewModel.selectedImages[idx])
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.tag(idx)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .automatic))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func publish() {
|
||||
viewModel.isPublishing = true
|
||||
viewModel.errorMessage = nil
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
viewModel.isPublishing = false
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
let apiService: any APIServiceProtocol & Sendable = LiveAPIService()
|
||||
do {
|
||||
// 1) 上传图片(如有)
|
||||
var resList: [ResListItem] = []
|
||||
if !viewModel.selectedImages.isEmpty {
|
||||
for image in viewModel.selectedImages {
|
||||
if let url = await COSManager.shared.uploadUIImage(image, apiService: apiService) {
|
||||
if let cg = image.cgImage {
|
||||
let item = ResListItem(resUrl: url, width: cg.width, height: cg.height, format: "jpeg")
|
||||
resList.append(item)
|
||||
} else {
|
||||
// 无法获取尺寸也允许发布,尺寸置为 0
|
||||
let item = ResListItem(resUrl: url, width: 0, height: 0, format: "jpeg")
|
||||
resList.append(item)
|
||||
}
|
||||
} else {
|
||||
viewModel.isPublishing = false
|
||||
viewModel.errorMessage = "图片上传失败"
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIKit Image Picker Wrapper
|
||||
private struct ImagePicker: UIViewControllerRepresentable {
|
||||
let sourceType: UIImagePickerController.SourceType
|
||||
let onImagePicked: (UIImage) -> Void
|
||||
// 2) 组装并发送发布请求
|
||||
let trimmed = viewModel.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let userId = await UserInfoManager.getCurrentUserId() ?? ""
|
||||
let type = resList.isEmpty ? "0" : "2" // 0: 纯文字, 2: 图片/图文
|
||||
let request = await PublishFeedRequest.make(
|
||||
content: trimmed,
|
||||
uid: userId,
|
||||
type: type,
|
||||
resList: resList.isEmpty ? nil : resList
|
||||
)
|
||||
let response = try await apiService.request(request)
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onImagePicked: onImagePicked)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = sourceType
|
||||
picker.allowsEditing = false
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||
|
||||
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
let onImagePicked: (UIImage) -> Void
|
||||
|
||||
init(onImagePicked: @escaping (UIImage) -> Void) {
|
||||
self.onImagePicked = onImagePicked
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
if let image = (info[.originalImage] as? UIImage) ?? (info[.editedImage] as? UIImage) {
|
||||
onImagePicked(image)
|
||||
// 3) 结果处理
|
||||
if response.code == 200 {
|
||||
viewModel.isPublishing = false
|
||||
NotificationCenter.default.post(name: .init("CreateFeedPublished"), object: nil)
|
||||
onDismiss()
|
||||
dismiss()
|
||||
} else {
|
||||
viewModel.isPublishing = false
|
||||
viewModel.errorMessage = response.message.isEmpty ? "发布失败" : response.message
|
||||
}
|
||||
} catch {
|
||||
viewModel.isPublishing = false
|
||||
viewModel.errorMessage = error.localizedDescription
|
||||
}
|
||||
picker.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
picker.dismiss(animated: true)
|
||||
private func removeImage(at index: Int) {
|
||||
guard viewModel.selectedImages.indices.contains(index) else { return }
|
||||
viewModel.selectedImages.remove(at: index)
|
||||
if isShowingPreview {
|
||||
if previewIndex >= viewModel.selectedImages.count { previewIndex = max(0, viewModel.selectedImages.count - 1) }
|
||||
if viewModel.selectedImages.isEmpty { isShowingPreview = false }
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import SwiftUI
|
||||
struct MainPage: View {
|
||||
@StateObject private var viewModel = MainViewModel()
|
||||
let onLogout: () -> Void
|
||||
@State private var isPresentingCreatePage: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $viewModel.navigationPath) {
|
||||
@@ -12,16 +13,18 @@ struct MainPage: View {
|
||||
ZStack {
|
||||
// 背景图片
|
||||
LoginBackgroundView()
|
||||
// 主内容
|
||||
mainContentView(geometry: geometry)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
// 主内容:使用 TabView 常驻子树
|
||||
TabView(selection: $viewModel.selectedTab) {
|
||||
MomentListHomePage(onCreateTapped: { isPresentingCreatePage = true })
|
||||
.tag(MainViewModel.Tab.feed)
|
||||
MePage(onLogout: onLogout)
|
||||
.tag(MainViewModel.Tab.me)
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
// 右上角按钮
|
||||
topRightButton
|
||||
}
|
||||
Spacer()
|
||||
// 底部导航栏(组件化)
|
||||
BottomTabBar(
|
||||
@@ -45,29 +48,6 @@ struct MainPage: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: String.self) { _ in EmptyView() }
|
||||
.navigationDestination(for: AppRoute.self) { route in
|
||||
switch route {
|
||||
case .setting:
|
||||
SettingPage(
|
||||
onBack: {
|
||||
viewModel.navigationPath.removeLast()
|
||||
},
|
||||
onLogout: {
|
||||
viewModel.onLogoutTapped()
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
case .publish:
|
||||
CreateFeedPage(
|
||||
onDismiss: {
|
||||
viewModel.navigationPath.removeLast()
|
||||
}
|
||||
)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.onLogout = onLogout
|
||||
@@ -77,73 +57,15 @@ struct MainPage: View {
|
||||
}
|
||||
viewModel.onAppear()
|
||||
}
|
||||
.fullScreenCover(isPresented: $isPresentingCreatePage) {
|
||||
CreateFeedPage {
|
||||
isPresentingCreatePage = false
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.isLoggedOut) { _, isLoggedOut in
|
||||
if isLoggedOut {
|
||||
onLogout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
private func mainContentView(geometry: GeometryProxy) -> some View {
|
||||
Group {
|
||||
switch viewModel.selectedTab {
|
||||
case .feed:
|
||||
MomentListHomePage()
|
||||
case .me:
|
||||
TempMePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部栏已组件化
|
||||
|
||||
// MARK: - 右上角按钮
|
||||
private var topRightButton: some View {
|
||||
Button(action: {
|
||||
viewModel.onTopRightButtonTapped()
|
||||
}) {
|
||||
Group {
|
||||
switch viewModel.selectedTab {
|
||||
case .feed:
|
||||
Image("add icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
case .me:
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 24, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - MeView (简化版本)
|
||||
|
||||
struct TempMePage: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Me View")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("This is a simplified MeView")
|
||||
.font(.body)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// MainPage(onLogout: {})
|
||||
//}
|
||||
|
45
yana/MVVM/MePage.swift
Normal file
45
yana/MVVM/MePage.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MePage: View {
|
||||
let onLogout: () -> Void
|
||||
@State private var isShowingSettings: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
VStack {
|
||||
Text("Me View")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("This is a simplified MeView")
|
||||
.font(.body)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
isShowingSettings = true
|
||||
}) {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 24, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.sheet(isPresented: $isShowingSettings) {
|
||||
SettingPage(
|
||||
onBack: { isShowingSettings = false },
|
||||
onLogout: {
|
||||
isShowingSettings = false
|
||||
onLogout()
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SplashV2: View {
|
||||
struct SplashPage: View {
|
||||
@State private var showLogin = false
|
||||
@State private var showMain = false
|
||||
@State private var hasCheckedAuth = false
|
@@ -15,65 +15,85 @@ struct MomentListBackgroundView: View {
|
||||
// MARK: - MomentListHomePage
|
||||
struct MomentListHomePage: View {
|
||||
@StateObject private var viewModel = MomentListHomeViewModel()
|
||||
let onCreateTapped: () -> Void
|
||||
|
||||
// MARK: - 图片预览状态
|
||||
@State private var previewItem: PreviewItem? = nil
|
||||
@State private var previewCurrentIndex: Int = 0
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景
|
||||
MomentListBackgroundView()
|
||||
// MARK: - 创建动态发布页弹窗
|
||||
// 迁移到上层(MainPage)统一管理,避免与 TabView 全屏弹窗冲突
|
||||
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 标题
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 背景
|
||||
MomentListBackgroundView()
|
||||
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 顶部标题居中 + 右上角添加按钮(垂直居中对齐)
|
||||
ZStack {
|
||||
// 居中标题
|
||||
Text(LocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 60)
|
||||
|
||||
// Volume 图标
|
||||
Image("Volume")
|
||||
.frame(width: 56, height: 41)
|
||||
.padding(.top, 16)
|
||||
// 右上角 “+” 按钮
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
debugInfoSync("➕ MomentListHomePage: 点击添加按钮")
|
||||
onCreateTapped()
|
||||
} label: {
|
||||
Image("add icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
}
|
||||
.frame(height: 56)
|
||||
|
||||
// 标语
|
||||
Text(LocalizedString("feedList.slogan",
|
||||
comment: ""))
|
||||
.font(.system(size: 16))
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
// 动态列表内容(Volume 与标语随列表滚动)
|
||||
if !viewModel.moments.isEmpty {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// Volume 图标 + 标语(随列表滚动)
|
||||
Image("Volume")
|
||||
.frame(width: 56, height: 41)
|
||||
.padding(.top, 16)
|
||||
Text(LocalizedString("feedList.slogan",
|
||||
comment: ""))
|
||||
.font(.system(size: 16))
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// 动态列表内容
|
||||
if !viewModel.moments.isEmpty {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(viewModel.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
MomentListItem(
|
||||
moment: moment,
|
||||
onImageTap: { images, tappedIndex in
|
||||
// 处理图片点击事件
|
||||
previewCurrentIndex = tappedIndex
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
debugInfoSync("📸 MomentListHomePage: 图片被点击")
|
||||
debugInfoSync(" 动态索引: \(index)")
|
||||
debugInfoSync(" 图片索引: \(tappedIndex)")
|
||||
debugInfoSync(" 图片数量: \(images.count)")
|
||||
}
|
||||
)
|
||||
.padding(.leading, 16)
|
||||
.padding(.trailing, 32)
|
||||
.onAppear {
|
||||
// 当显示倒数第三个项目时,开始加载更多
|
||||
if index == viewModel.moments.count - 3 {
|
||||
viewModel.loadMoreData()
|
||||
}
|
||||
ForEach(Array(viewModel.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
MomentListItem(
|
||||
moment: moment,
|
||||
onImageTap: { images, tappedIndex in
|
||||
// 处理图片点击事件
|
||||
previewCurrentIndex = tappedIndex
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
debugInfoSync("📸 MomentListHomePage: 图片被点击")
|
||||
debugInfoSync(" 动态索引: \(index)")
|
||||
debugInfoSync(" 图片索引: \(tappedIndex)")
|
||||
debugInfoSync(" 图片数量: \(images.count)")
|
||||
}
|
||||
)
|
||||
.padding(.leading, 16)
|
||||
.padding(.trailing, 32)
|
||||
.onAppear {
|
||||
// 当显示倒数第三个项目时,开始加载更多
|
||||
if index == viewModel.moments.count - 3 {
|
||||
viewModel.loadMoreData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多状态指示器
|
||||
if viewModel.isLoadingMore {
|
||||
@@ -98,55 +118,58 @@ struct MomentListHomePage: View {
|
||||
}
|
||||
.padding(.bottom, 160) // 为底部导航栏留出空间
|
||||
}
|
||||
.refreshable {
|
||||
// 下拉刷新
|
||||
viewModel.refreshData()
|
||||
}
|
||||
.onAppear {
|
||||
// 调试信息
|
||||
debugInfoSync("📱 MomentListHomePage: 显示动态列表")
|
||||
debugInfoSync(" 动态数量: \(viewModel.moments.count)")
|
||||
debugInfoSync(" 是否有更多: \(viewModel.hasMore)")
|
||||
debugInfoSync(" 是否正在加载更多: \(viewModel.isLoadingMore)")
|
||||
}
|
||||
} else if viewModel.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
} else if let error = viewModel.error {
|
||||
VStack(spacing: 16) {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// 重试按钮
|
||||
Button(action: {
|
||||
viewModel.refreshData()
|
||||
}) {
|
||||
Text("重试")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.white.opacity(0.2))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
.refreshable {
|
||||
// 下拉刷新
|
||||
viewModel.refreshData()
|
||||
}
|
||||
.onAppear {
|
||||
// 调试信息
|
||||
debugInfoSync("📱 MomentListHomePage: 显示动态列表")
|
||||
debugInfoSync(" 动态数量: \(viewModel.moments.count)")
|
||||
debugInfoSync(" 是否有更多: \(viewModel.hasMore)")
|
||||
debugInfoSync(" 是否正在加载更多: \(viewModel.isLoadingMore)")
|
||||
}
|
||||
} else if viewModel.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
} else if let error = viewModel.error {
|
||||
VStack(spacing: 16) {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
Spacer()
|
||||
// 重试按钮
|
||||
Button(action: {
|
||||
viewModel.refreshData()
|
||||
}) {
|
||||
Text("重试")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.white.opacity(0.2))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.safeAreaPadding(.top, 8)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
viewModel.onAppear()
|
||||
}
|
||||
// MARK: - 图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
.onReceive(NotificationCenter.default.publisher(for: .init("CreateFeedPublished"))) { _ in
|
||||
viewModel.refreshData()
|
||||
}
|
||||
// MARK: - 图片预览弹窗(使用 sheet 以避免与发布页全屏弹窗冲突)
|
||||
.sheet(item: $previewItem) { item in
|
||||
ImagePreviewPager(
|
||||
images: item.images as [String],
|
||||
currentIndex: $previewCurrentIndex
|
||||
@@ -155,5 +178,6 @@ struct MomentListHomePage: View {
|
||||
debugInfoSync("📸 MomentListHomePage: 图片预览已关闭")
|
||||
}
|
||||
}
|
||||
// 发布页由上层统一控制
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,6 @@ struct MomentListItem: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 背景层
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.clear)
|
||||
.overlay(
|
||||
|
123
yana/MVVM/View/NineGridImagePicker.swift
Normal file
123
yana/MVVM/View/NineGridImagePicker.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
struct NineGridImagePicker: View {
|
||||
@Binding var images: [UIImage]
|
||||
var maxCount: Int = 9
|
||||
var cornerRadius: CGFloat = 16
|
||||
var spacing: CGFloat = 8
|
||||
var horizontalPadding: CGFloat = 20
|
||||
var onTapImage: (Int) -> Void = { _ in }
|
||||
|
||||
@State private var pickerItems: [PhotosPickerItem] = []
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let columns = Array(repeating: GridItem(.flexible(), spacing: spacing), count: 3)
|
||||
let columnsCount: CGFloat = 3
|
||||
let totalSpacing = spacing * (columnsCount - 1)
|
||||
let availableWidth = geometry.size.width - horizontalPadding * 2
|
||||
let cellSide = (availableWidth - totalSpacing) / columnsCount
|
||||
|
||||
LazyVGrid(columns: columns, spacing: spacing) {
|
||||
ForEach(0..<maxCount, id: \.self) { index in
|
||||
ZStack {
|
||||
// 占位背景(仅 DEBUG 可见)
|
||||
#if DEBUG
|
||||
if index >= images.count && !(index == images.count && images.count < maxCount) {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color.white.opacity(0.08))
|
||||
}
|
||||
#endif
|
||||
|
||||
if index < images.count {
|
||||
// 图片格子
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: images[index])
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.contentShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||
.onTapGesture { onTapImage(index) }
|
||||
|
||||
Button {
|
||||
removeImage(at: index)
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.background(Circle().fill(Color.black.opacity(0.4)))
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
}
|
||||
.padding(6)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
} else if index == images.count && images.count < maxCount {
|
||||
// 添加按钮格子
|
||||
PhotosPicker(
|
||||
selection: $pickerItems,
|
||||
maxSelectionCount: maxCount - images.count,
|
||||
selectionBehavior: .ordered,
|
||||
matching: .images
|
||||
) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color(hex: 0x1C143A))
|
||||
Image(systemName: "plus")
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.font(.system(size: 32, weight: .semibold))
|
||||
}
|
||||
}
|
||||
.onChange(of: pickerItems) { _, newItems in
|
||||
handlePickerItems(newItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: cellSide)
|
||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||
.contentShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
.frame(height: gridHeight(forCount: max(images.count, 1)))
|
||||
}
|
||||
|
||||
private func gridHeight(forCount count: Int) -> CGFloat {
|
||||
// 通过一个近似:用屏幕宽度估算高度以确保父布局正确测量。
|
||||
// 每行 3 个,行数 = ceil(count / 3.0)。在 GeometryReader 中真实高度会覆盖此近似。
|
||||
let screenWidth = UIScreen.main.bounds.width
|
||||
let columnsCount: CGFloat = 3
|
||||
let totalSpacing = spacing * (columnsCount - 1)
|
||||
let availableWidth = screenWidth - horizontalPadding * 2
|
||||
let side = (availableWidth - totalSpacing) / columnsCount
|
||||
let rows = ceil(CGFloat(count) / 3.0)
|
||||
let totalRowSpacing = spacing * max(rows - 1, 0)
|
||||
return side * rows + totalRowSpacing
|
||||
}
|
||||
|
||||
private func handlePickerItems(_ items: [PhotosPickerItem]) {
|
||||
guard !items.isEmpty else { return }
|
||||
Task { @MainActor in
|
||||
var appended: [UIImage] = []
|
||||
for item in items {
|
||||
if images.count + appended.count >= maxCount { break }
|
||||
if let data = try? await item.loadTransferable(type: Data.self),
|
||||
let image = UIImage(data: data) {
|
||||
appended.append(image)
|
||||
}
|
||||
}
|
||||
if !appended.isEmpty {
|
||||
images.append(contentsOf: appended)
|
||||
}
|
||||
pickerItems = []
|
||||
}
|
||||
}
|
||||
|
||||
private func removeImage(at index: Int) {
|
||||
guard images.indices.contains(index) else { return }
|
||||
images.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,78 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Tab 枚举
|
||||
enum Tab: Int, CaseIterable {
|
||||
case feed = 0
|
||||
case me = 1
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "动态"
|
||||
case .me:
|
||||
return "我的"
|
||||
}
|
||||
}
|
||||
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "feed unselected"
|
||||
case .me:
|
||||
return "me unselected"
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIconName: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "feed selected"
|
||||
case .me:
|
||||
return "me selected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BottomTabView 组件
|
||||
struct BottomTabView: View {
|
||||
@Binding var selectedTab: Tab
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(Tab.allCases, id: \.rawValue) { tab in
|
||||
Button(action: {
|
||||
selectedTab = tab
|
||||
}) {
|
||||
Image(selectedTab == tab ? tab.selectedIconName : tab.iconName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
.frame(height: 60)
|
||||
.padding(.horizontal, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 30)
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 30)
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 0.5)
|
||||
)
|
||||
.shadow(
|
||||
color: Color.black.opacity(0.34),
|
||||
radius: 10.7,
|
||||
x: 0,
|
||||
y: 1.9
|
||||
)
|
||||
)
|
||||
.padding(.horizontal, 15)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
BottomTabView(selectedTab: .constant(.feed))
|
||||
.background(Color.purple) // 预览时添加背景色以便查看效果
|
||||
}
|
@@ -23,7 +23,7 @@ struct yanaApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
SplashV2()
|
||||
SplashPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user