feat: 添加发布动态功能及相关视图组件
- 在APIEndpoints.swift中新增publishFeed端点以支持发布动态。 - 新增PublishFeedRequest和PublishFeedResponse模型,处理发布请求和响应。 - 在EditFeedFeature中实现动态编辑功能,支持用户输入和发布内容。 - 更新CreateFeedView和EditFeedView以集成新的发布功能,提升用户体验。 - 在Localizable.strings中添加相关文本的本地化支持,确保多语言兼容性。 - 优化FeedListView和FeedView以展示最新动态,增强用户交互体验。
This commit is contained in:
@@ -21,10 +21,13 @@ enum APIEndpoint: String, CaseIterable {
|
||||
case emailGetCode = "/email/getCode" // 新增:邮箱验证码获取端点
|
||||
case latestDynamics = "/dynamic/square/latestDynamics" // 新增:获取最新动态列表端点
|
||||
case tcToken = "/tencent/cos/getToken" // 新增:腾讯云 COS Token 获取端点
|
||||
case publishFeed = "/dynamic/square/publish" // 发布动态
|
||||
|
||||
// Web 页面路径
|
||||
case userAgreement = "/modules/rule/protocol.html"
|
||||
case privacyPolicy = "/modules/rule/privacy-wap.html"
|
||||
|
||||
|
||||
var path: String {
|
||||
return self.rawValue
|
||||
}
|
||||
|
@@ -158,3 +158,69 @@ struct LatestDynamicsRequest: APIRequestProtocol {
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
}
|
||||
|
||||
// MARK: - 发布动态 API 请求与响应
|
||||
|
||||
/// 发布动态请求
|
||||
struct PublishFeedRequest: APIRequestProtocol {
|
||||
typealias Response = PublishFeedResponse
|
||||
|
||||
let endpoint: String = APIEndpoint.publishFeed.path
|
||||
let method: HTTPMethod = .POST
|
||||
|
||||
let content: String
|
||||
let uid: String
|
||||
let type: String
|
||||
var pub_sign: String
|
||||
|
||||
var queryParameters: [String: String]? { nil }
|
||||
var bodyParameters: [String: Any]? {
|
||||
[
|
||||
"content": content,
|
||||
"uid": uid,
|
||||
"type": type,
|
||||
"pub_sign": pub_sign
|
||||
]
|
||||
}
|
||||
var includeBaseParameters: Bool { true }
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
|
||||
/// async 工厂方法,主线程生成 pub_sign
|
||||
static func make(content: String, uid: String, type: String = "0") async -> PublishFeedRequest {
|
||||
let base = await MainActor.run { BaseRequest() }
|
||||
var mutableBase = base
|
||||
mutableBase.generateSignature(with: [
|
||||
"content": content,
|
||||
"uid": uid,
|
||||
"type": type
|
||||
])
|
||||
return PublishFeedRequest(
|
||||
content: content,
|
||||
uid: uid,
|
||||
type: type,
|
||||
pub_sign: mutableBase.pubSign
|
||||
)
|
||||
}
|
||||
|
||||
/// 禁止外部直接调用
|
||||
private init(content: String, uid: String, type: String, pub_sign: String) {
|
||||
self.content = content
|
||||
self.uid = uid
|
||||
self.type = type
|
||||
self.pub_sign = pub_sign
|
||||
}
|
||||
}
|
||||
|
||||
/// 发布动态响应
|
||||
struct PublishFeedResponse: Codable, Equatable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: PublishFeedData?
|
||||
let timestamp: Int?
|
||||
}
|
||||
|
||||
/// 发布动态返回数据
|
||||
struct PublishFeedData: Codable, Equatable {
|
||||
let dynamicId: Int?
|
||||
}
|
||||
|
@@ -2,7 +2,9 @@ import UIKit
|
||||
//import NIMSDK
|
||||
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) async -> Bool {
|
||||
private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) async -> Bool {
|
||||
|
||||
// isPerceptionCheckingEnabled = false
|
||||
|
||||
// 执行数据迁移(从 UserDefaults 到 Keychain)
|
||||
DataMigrationManager.performStartupMigration()
|
||||
|
89
yana/Features/EditFeedFeature.swift
Normal file
89
yana/Features/EditFeedFeature.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
import SwiftUI
|
||||
|
||||
@Reducer
|
||||
struct EditFeedFeature {
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var content: String = ""
|
||||
var isLoading: Bool = false
|
||||
var errorMessage: String? = nil
|
||||
var shouldDismiss: Bool = false
|
||||
|
||||
var canPublish: Bool {
|
||||
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
|
||||
}
|
||||
}
|
||||
|
||||
enum Action {
|
||||
case contentChanged(String)
|
||||
case publishButtonTapped
|
||||
case publishResponse(Result<PublishFeedResponse, Error>)
|
||||
case clearError
|
||||
case dismissView
|
||||
case clearDismissFlag
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@Dependency(\.dismiss) var dismiss
|
||||
@Dependency(\.isPresented) var isPresented
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .contentChanged(let newContent):
|
||||
state.content = newContent
|
||||
return .none
|
||||
|
||||
case .publishButtonTapped:
|
||||
guard state.canPublish else {
|
||||
state.errorMessage = "请输入内容"
|
||||
return .none
|
||||
}
|
||||
state.isLoading = true
|
||||
state.errorMessage = nil
|
||||
|
||||
return .run { [content = state.content] send in
|
||||
let userId = await UserInfoManager.getCurrentUserId() ?? ""
|
||||
let request = await PublishFeedRequest.make(
|
||||
content: content.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
uid: userId,
|
||||
type: "0"
|
||||
)
|
||||
do {
|
||||
let response = try await apiService.request(request)
|
||||
await send(.publishResponse(.success(response)))
|
||||
} catch {
|
||||
await send(.publishResponse(.failure(error)))
|
||||
}
|
||||
}
|
||||
|
||||
case .publishResponse(.success(let response)):
|
||||
state.isLoading = false
|
||||
if response.code == 200 {
|
||||
return .send(.dismissView)
|
||||
} else {
|
||||
state.errorMessage = response.message.isEmpty ? "发布失败" : response.message
|
||||
return .none
|
||||
}
|
||||
|
||||
case .publishResponse(.failure(let error)):
|
||||
state.isLoading = false
|
||||
state.errorMessage = error.localizedDescription
|
||||
return .none
|
||||
|
||||
case .clearError:
|
||||
state.errorMessage = nil
|
||||
return .none
|
||||
|
||||
case .dismissView:
|
||||
state.shouldDismiss = true
|
||||
return .none
|
||||
case .clearDismissFlag:
|
||||
state.shouldDismiss = false
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,11 +2,10 @@
|
||||
Localizable.strings
|
||||
yana
|
||||
|
||||
Created on 2024.
|
||||
英文本地化文件
|
||||
English localization file (auto-aligned)
|
||||
*/
|
||||
|
||||
// MARK: - 登录界面
|
||||
// MARK: - Login Screen
|
||||
"login.id_login" = "ID Login";
|
||||
"login.email_login" = "Email Login";
|
||||
"login.app_title" = "E-PARTI";
|
||||
@@ -14,32 +13,32 @@
|
||||
"login.agreement" = "User Service Agreement";
|
||||
"login.policy" = "Privacy Policy";
|
||||
|
||||
// MARK: - 通用按钮
|
||||
// MARK: - Common Buttons
|
||||
"common.login" = "Login";
|
||||
"common.register" = "Register";
|
||||
"common.cancel" = "Cancel";
|
||||
"common.confirm" = "Confirm";
|
||||
"common.ok" = "OK";
|
||||
|
||||
// MARK: - 错误信息
|
||||
// MARK: - Error Messages
|
||||
"error.network" = "Network Error";
|
||||
"error.invalid_input" = "Invalid Input";
|
||||
"error.login_failed" = "Login Failed";
|
||||
|
||||
// MARK: - 占位符文本
|
||||
// MARK: - Placeholders
|
||||
"placeholder.email" = "Enter your email";
|
||||
"placeholder.password" = "Enter your password";
|
||||
"placeholder.username" = "Enter your username";
|
||||
"placeholder.enter_id" = "Please enter ID";
|
||||
"placeholder.enter_password" = "Please enter password";
|
||||
|
||||
// MARK: - ID登录页面
|
||||
// MARK: - ID Login Page
|
||||
"id_login.title" = "ID Login";
|
||||
"id_login.forgot_password" = "Forgot Password?";
|
||||
"id_login.login_button" = "Login";
|
||||
"id_login.logging_in" = "Logging in...";
|
||||
|
||||
// MARK: - 邮箱登录页面
|
||||
// MARK: - Email Login Page
|
||||
"email_login.title" = "Email Login";
|
||||
"email_login.email_required" = "Please enter email";
|
||||
"email_login.invalid_email" = "Please enter a valid email address";
|
||||
@@ -52,13 +51,13 @@
|
||||
"placeholder.enter_email" = "Please enter email";
|
||||
"placeholder.enter_verification_code" = "Please enter verification code";
|
||||
|
||||
// MARK: - 验证和错误信息
|
||||
// MARK: - Validation and Error Messages
|
||||
"validation.id_required" = "Please enter your ID";
|
||||
"validation.password_required" = "Please enter your password";
|
||||
"error.encryption_failed" = "Encryption failed, please try again";
|
||||
"error.login_failed" = "Login failed, please check your credentials";
|
||||
|
||||
// MARK: - 密码恢复页面
|
||||
// MARK: - Password Recovery Page
|
||||
"recover_password.title" = "Recover Password";
|
||||
"recover_password.placeholder_email" = "Please enter email";
|
||||
"recover_password.placeholder_verification_code" = "Please enter verification code";
|
||||
@@ -74,5 +73,49 @@
|
||||
"recover_password.reset_success" = "Password reset successfully";
|
||||
"recover_password.resetting" = "Resetting...";
|
||||
|
||||
// MARK: - 主页
|
||||
// MARK: - Home
|
||||
"home.title" = "Enjoy your Life Time";
|
||||
|
||||
// MARK: - Create Feed
|
||||
"createFeed.enterContent" = "Enter Content";
|
||||
"createFeed.processingImages" = "Processing images...";
|
||||
"createFeed.publishing" = "Publishing...";
|
||||
"createFeed.publish" = "Publish";
|
||||
"createFeed.title" = "Image & Text Publish";
|
||||
|
||||
// MARK: - Edit Feed
|
||||
"editFeed.title" = "Image & Text Edit";
|
||||
"editFeed.publish" = "Publish";
|
||||
"editFeed.enterContent" = "Enter Content";
|
||||
|
||||
// MARK: - Feed List
|
||||
"feedList.title" = "Enjoy your Life Time";
|
||||
"feedList.slogan" = "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.";
|
||||
|
||||
// MARK: - Feed
|
||||
"feed.title" = "Enjoy your Life Time";
|
||||
"feed.empty" = "No moments yet";
|
||||
"feed.error" = "Error: %@";
|
||||
"feed.retry" = "Retry";
|
||||
"feed.loadingMore" = "Loading more...";
|
||||
"me.title" = "Me";
|
||||
"me.nickname" = "Nickname";
|
||||
"me.id" = "ID: %@";
|
||||
"language.select" = "Select Language";
|
||||
"language.current" = "Current Language";
|
||||
"language.info" = "Language Info";
|
||||
"feed.user" = "User %d";
|
||||
"feed.2hoursago" = "2 hours ago";
|
||||
"feed.demoContent" = "Today is a beautiful day, sharing some little happiness in life. Hope everyone cherishes every moment.";
|
||||
"feed.vip" = "VIP%d";
|
||||
|
||||
// MARK: - Splash
|
||||
"splash.title" = "E-Parti";
|
||||
|
||||
// MARK: - Setting
|
||||
"setting.title" = "Settings";
|
||||
"setting.user" = "User";
|
||||
"setting.language" = "Language Settings";
|
||||
"setting.about" = "About Us";
|
||||
"setting.version" = "Version Info";
|
||||
"setting.logout" = "Logout";
|
@@ -76,3 +76,42 @@
|
||||
|
||||
// MARK: - 主页
|
||||
"home.title" = "享受您的生活时光";
|
||||
|
||||
"createFeed.enterContent" = "输入内容";
|
||||
"createFeed.processingImages" = "处理图片中...";
|
||||
"createFeed.publishing" = "发布中...";
|
||||
"createFeed.publish" = "发布";
|
||||
"createFeed.title" = "图文发布";
|
||||
|
||||
"editFeed.title" = "图文发布";
|
||||
"editFeed.publish" = "发布";
|
||||
"editFeed.enterContent" = "输入内容";
|
||||
|
||||
"feedList.title" = "享受您的生活时光";
|
||||
"feedList.slogan" = "疾病如同残酷的统治者,\n而时间是我们最宝贵的财富。\n我们活着的每一刻,都是对不可避免命运的胜利。";
|
||||
|
||||
"feed.title" = "享受您的生活时光";
|
||||
"feed.empty" = "暂无动态内容";
|
||||
"feed.error" = "错误: %@";
|
||||
"feed.retry" = "重试";
|
||||
"feed.loadingMore" = "加载更多...";
|
||||
|
||||
"splash.title" = "E-Parti";
|
||||
|
||||
"setting.title" = "设置";
|
||||
"setting.user" = "用户";
|
||||
"setting.language" = "语言设置";
|
||||
"setting.about" = "关于我们";
|
||||
"setting.version" = "版本信息";
|
||||
"setting.logout" = "退出登录";
|
||||
|
||||
"me.title" = "我的";
|
||||
"me.nickname" = "用户昵称";
|
||||
"me.id" = "ID: %@";
|
||||
"language.select" = "选择语言";
|
||||
"language.current" = "当前语言";
|
||||
"language.info" = "语言信息";
|
||||
"feed.user" = "用户%d";
|
||||
"feed.2hoursago" = "2小时前";
|
||||
"feed.demoContent" = "今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。";
|
||||
"feed.vip" = "VIP%d";
|
||||
|
@@ -23,4 +23,28 @@ extension Color {
|
||||
let blue = Double(hex & 0xFF) / 255.0
|
||||
self.init(red: red, green: green, blue: blue, opacity: alpha)
|
||||
}
|
||||
|
||||
init(hexString: String) {
|
||||
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (255, 0, 0, 0)
|
||||
}
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
@@ -25,7 +25,7 @@ struct CreateFeedView: View {
|
||||
.frame(height: 200) // 高度固定为200
|
||||
|
||||
if store.content.isEmpty {
|
||||
Text("Enter Content")
|
||||
Text(NSLocalizedString("createFeed.enterContent", comment: "Enter Content"))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
@@ -79,7 +79,7 @@ struct CreateFeedView: View {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
Text("处理图片中...")
|
||||
Text(NSLocalizedString("createFeed.processingImages", comment: "Processing images..."))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
@@ -111,11 +111,11 @@ struct CreateFeedView: View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
Text("发布中...")
|
||||
Text(NSLocalizedString("createFeed.publishing", comment: "Publishing..."))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("发布")
|
||||
Text(NSLocalizedString("createFeed.publish", comment: "Publish"))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
@@ -137,7 +137,7 @@ struct CreateFeedView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle("图文发布")
|
||||
.navigationTitle(NSLocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
|
@@ -1,19 +1,195 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct EditFeedView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Text("编辑动态")
|
||||
.font(.title)
|
||||
.bold()
|
||||
Text("这里是 EditFeedView 占位内容")
|
||||
.foregroundColor(.gray)
|
||||
Spacer()
|
||||
let onDismiss: () -> Void
|
||||
let store: StoreOf<EditFeedFeature>
|
||||
@State private var isKeyboardVisible = false
|
||||
private let maxCount = 500
|
||||
|
||||
private func hideKeyboard() {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
backgroundView
|
||||
mainContent(geometry: geometry, viewStore: viewStore)
|
||||
// if viewStore.isLoading {
|
||||
// loadingOverlay
|
||||
// }
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if isKeyboardVisible {
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
isKeyboardVisible = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
isKeyboardVisible = false
|
||||
}
|
||||
}
|
||||
.onChange(of: viewStore.errorMessage) { error in
|
||||
if error != nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
viewStore.send(.clearError)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: viewStore.shouldDismiss) { shouldDismiss in
|
||||
if shouldDismiss {
|
||||
onDismiss()
|
||||
viewStore.send(.clearDismissFlag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
EditFeedView()
|
||||
private var backgroundView: some View {
|
||||
Color(hexString: "0C0527")
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
private func mainContent(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
headerView(geometry: geometry, viewStore: viewStore)
|
||||
textInputArea(viewStore: viewStore)
|
||||
Spacer()
|
||||
if !isKeyboardVisible {
|
||||
publishButtonBottom(viewStore: viewStore, geometry: geometry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func headerView(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||||
HStack {
|
||||
Text(NSLocalizedString("editFeed.title", comment: "Image & Text Edit"))
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
if isKeyboardVisible {
|
||||
WithPerceptionTracking {
|
||||
Button(action: {
|
||||
hideKeyboard()
|
||||
viewStore.send(.publishButtonTapped)
|
||||
}) {
|
||||
Text(NSLocalizedString("editFeed.publish", comment: "Publish"))
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(hexString: "A14AC6"),
|
||||
Color(hexString: "3B1EEB")
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.cornerRadius(16)
|
||||
)
|
||||
}
|
||||
.disabled(!viewStore.canPublish)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, geometry.safeAreaInsets.top + 16)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
|
||||
private func textInputArea(viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(Color(hexString: "1C143A"))
|
||||
TextEditor(text: Binding(
|
||||
get: { viewStore.content },
|
||||
set: { viewStore.send(.contentChanged($0)) }
|
||||
))
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding(16)
|
||||
.frame(height: 160)
|
||||
.foregroundColor(.white)
|
||||
.background(.clear)
|
||||
.cornerRadius(20)
|
||||
.font(.system(size: 16))
|
||||
if viewStore.content.isEmpty {
|
||||
Text(NSLocalizedString("editFeed.enterContent", comment: "Enter Content"))
|
||||
.foregroundColor(Color.white.opacity(0.4))
|
||||
.padding(20)
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
WithPerceptionTracking {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("\(viewStore.content.count)/\(maxCount)")
|
||||
.foregroundColor(Color.white.opacity(0.4))
|
||||
.font(.system(size: 14))
|
||||
.padding(.trailing, 16)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 160)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
|
||||
private func publishButtonBottom(viewStore: ViewStoreOf<EditFeedFeature>, geometry: GeometryProxy) -> some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
hideKeyboard()
|
||||
viewStore.send(.publishButtonTapped)
|
||||
}) {
|
||||
Text(NSLocalizedString("editFeed.publish", comment: "Publish"))
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 56)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [Color(hexString: "A14AC6"), Color(hexString: "3B1EEB")],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.cornerRadius(28)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 24)
|
||||
.disabled(!viewStore.canPublish)
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingOverlay: some View {
|
||||
Group {
|
||||
Color.black.opacity(0.3)
|
||||
.ignoresSafeArea()
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// EditFeedView()
|
||||
//}
|
||||
|
@@ -20,7 +20,7 @@ struct FeedListView: View {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Spacer(minLength: 0)
|
||||
Text("Enjoy your Life Time")
|
||||
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
@@ -40,7 +40,7 @@ struct FeedListView: View {
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 40)
|
||||
Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.")
|
||||
Text(NSLocalizedString("feedList.slogan", comment: "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable."))
|
||||
.font(.system(size: 16))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
@@ -58,7 +58,16 @@ struct FeedListView: View {
|
||||
get: \.isEditFeedPresented,
|
||||
send: { $0 ? .editFeedButtonTapped : .editFeedDismissed }
|
||||
)) {
|
||||
EditFeedView()
|
||||
EditFeedView(
|
||||
onDismiss: {
|
||||
viewStore.send(.editFeedDismissed)
|
||||
},
|
||||
store: Store(
|
||||
initialState: EditFeedFeature.State()
|
||||
) {
|
||||
EditFeedFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,12 +8,12 @@ struct FeedTopBarView: View {
|
||||
WithPerceptionTracking {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Enjoy your Life Time")
|
||||
Text(NSLocalizedString("feed.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
onShowCreateFeed() // 只调用回调
|
||||
// showEditFeed = true // 显示编辑界面
|
||||
}) {
|
||||
Image("add icon")
|
||||
.frame(width: 36, height: 36)
|
||||
@@ -34,11 +34,11 @@ struct FeedMomentsListView: View {
|
||||
Image(systemName: "heart.text.square")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
Text("暂无动态内容")
|
||||
Text(NSLocalizedString("feed.empty", comment: "No moments yet"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
if let error = store.error {
|
||||
Text("错误: \(error)")
|
||||
Text(String(format: NSLocalizedString("feed.error", comment: "Error: %@"), error))
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.red.opacity(0.8))
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -50,7 +50,7 @@ struct FeedMomentsListView: View {
|
||||
Button(action: {
|
||||
store.send(.retryLoad)
|
||||
}) {
|
||||
Text("重试")
|
||||
Text(NSLocalizedString("feed.retry", comment: "Retry"))
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 20)
|
||||
@@ -85,7 +85,7 @@ struct FeedMomentsListView: View {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
Text("加载更多...")
|
||||
Text(NSLocalizedString("feed.loadingMore", comment: "Loading more..."))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
@@ -102,6 +102,7 @@ struct FeedMomentsListView: View {
|
||||
struct FeedView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
let onShowCreateFeed: () -> Void
|
||||
@State private var showEditFeed = false
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
@@ -154,6 +155,18 @@ struct FeedView: View {
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.sheet(isPresented: $showEditFeed) {
|
||||
EditFeedView(
|
||||
onDismiss: {
|
||||
showEditFeed = false
|
||||
},
|
||||
store: Store(
|
||||
initialState: EditFeedFeature.State()
|
||||
) {
|
||||
EditFeedFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ struct SettingView: View {
|
||||
Spacer()
|
||||
|
||||
// 标题
|
||||
Text("设置")
|
||||
Text(NSLocalizedString("setting.title", comment: "Settings"))
|
||||
.font(.custom("PingFang SC-Semibold", size: 16))
|
||||
.foregroundColor(.white)
|
||||
|
||||
@@ -47,7 +47,43 @@ struct SettingView: View {
|
||||
// 内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// 用户信息卡片
|
||||
UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel)
|
||||
// .padding()
|
||||
.padding(.top, 32)
|
||||
|
||||
SettingOptionsView(
|
||||
onLanguageTapped: {
|
||||
// TODO: 实现语言设置
|
||||
},
|
||||
onAboutTapped: {
|
||||
// TODO: 实现关于页面
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(minLength: 50)
|
||||
|
||||
LogoutButtonView {
|
||||
store.send(.logoutTapped)
|
||||
}
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Info Card View
|
||||
struct UserInfoCardView: View {
|
||||
let userInfo: UserInfo?
|
||||
let accountModel: AccountModel?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
// 头像区域
|
||||
Image(systemName: "person.circle.fill")
|
||||
@@ -56,22 +92,22 @@ struct SettingView: View {
|
||||
|
||||
// 用户信息
|
||||
VStack(spacing: 8) {
|
||||
if let userInfo = store.userInfo, let userName = userInfo.username {
|
||||
if let userInfo = userInfo, let userName = userInfo.username {
|
||||
Text(userName)
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("用户")
|
||||
Text(NSLocalizedString("setting.user", comment: "User"))
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// 显示用户ID
|
||||
if let userInfo = store.userInfo, let userId = userInfo.userId {
|
||||
if let userInfo = userInfo, let userId = userInfo.userId {
|
||||
Text("ID: \(userId)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
} else if let accountModel = store.accountModel, let uid = accountModel.uid {
|
||||
} else if let accountModel = accountModel, let uid = accountModel.uid {
|
||||
Text("UID: \(uid)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
@@ -83,35 +119,34 @@ struct SettingView: View {
|
||||
.background(Color.black.opacity(0.3))
|
||||
.cornerRadius(16)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 32)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置选项列表
|
||||
// MARK: - Setting Options View
|
||||
struct SettingOptionsView: View {
|
||||
let onLanguageTapped: () -> Void
|
||||
let onAboutTapped: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
// 语言设置
|
||||
SettingRowView(
|
||||
icon: "globe",
|
||||
title: "语言设置",
|
||||
action: {
|
||||
// TODO: 实现语言设置
|
||||
}
|
||||
title: NSLocalizedString("setting.language", comment: "Language Settings"),
|
||||
action: onLanguageTapped
|
||||
)
|
||||
|
||||
// 关于我们
|
||||
SettingRowView(
|
||||
icon: "info.circle",
|
||||
title: "关于我们",
|
||||
action: {
|
||||
// TODO: 实现关于页面
|
||||
}
|
||||
title: NSLocalizedString("setting.about", comment: "About Us"),
|
||||
action: onAboutTapped
|
||||
)
|
||||
|
||||
// 版本信息
|
||||
HStack {
|
||||
Image(systemName: "app.badge")
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.frame(width: 24)
|
||||
|
||||
Text("版本信息")
|
||||
Text(NSLocalizedString("setting.version", comment: "Version Info"))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
@@ -126,16 +161,18 @@ struct SettingView: View {
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 50)
|
||||
// MARK: - Logout Button View
|
||||
struct LogoutButtonView: View {
|
||||
let action: () -> Void
|
||||
|
||||
// 退出登录按钮
|
||||
Button(action: {
|
||||
store.send(.logoutTapped)
|
||||
}) {
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.right.square")
|
||||
Text("退出登录")
|
||||
Text(NSLocalizedString("setting.logout", comment: "Logout"))
|
||||
}
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(.white)
|
||||
@@ -145,16 +182,6 @@ struct SettingView: View {
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -64,7 +64,7 @@ struct SplashView: View {
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
// 应用标题 - 白色,40pt字体
|
||||
Text("E-Parti")
|
||||
Text(NSLocalizedString("splash.title", comment: "E-Parti"))
|
||||
.font(.system(size: 40, weight: .regular))
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
Reference in New Issue
Block a user