feat: 添加发布动态功能及相关视图组件

- 在APIEndpoints.swift中新增publishFeed端点以支持发布动态。
- 新增PublishFeedRequest和PublishFeedResponse模型,处理发布请求和响应。
- 在EditFeedFeature中实现动态编辑功能,支持用户输入和发布内容。
- 更新CreateFeedView和EditFeedView以集成新的发布功能,提升用户体验。
- 在Localizable.strings中添加相关文本的本地化支持,确保多语言兼容性。
- 优化FeedListView和FeedView以展示最新动态,增强用户交互体验。
This commit is contained in:
edwinQQQ
2025-07-22 15:13:32 +08:00
parent d4bef537d9
commit 6c363ea884
13 changed files with 624 additions and 133 deletions

View File

@@ -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
}

View File

@@ -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?
}

View File

@@ -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()

View 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
}
}
}
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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
)
}
}

View File

@@ -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)

View File

@@ -1,19 +1,195 @@
import SwiftUI
import ComposableArchitecture
struct EditFeedView: View {
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 {
VStack(spacing: 20) {
Text("编辑动态")
.font(.title)
.bold()
Text("这里是 EditFeedView 占位内容")
.foregroundColor(.gray)
Spacer()
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)
}
}
}
}
}
}
}
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)
}
.padding()
}
}
#Preview {
EditFeedView()
}
//#Preview {
// EditFeedView()
//}

View File

@@ -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()
}
)
}
}
}

View File

@@ -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()
}
)
}
}
}
}

View File

@@ -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,104 +47,24 @@ struct SettingView: View {
//
ScrollView {
VStack(spacing: 24) {
//
VStack(spacing: 16) {
//
Image(systemName: "person.circle.fill")
.font(.system(size: 60))
.foregroundColor(.white.opacity(0.8))
UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel)
// .padding()
.padding(.top, 32)
//
VStack(spacing: 8) {
if let userInfo = store.userInfo, let userName = userInfo.username {
Text(userName)
.font(.title2.weight(.semibold))
.foregroundColor(.white)
} else {
Text("用户")
.font(.title2.weight(.semibold))
.foregroundColor(.white)
}
// ID
if let userInfo = store.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 {
Text("UID: \(uid)")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
}
SettingOptionsView(
onLanguageTapped: {
// TODO:
},
onAboutTapped: {
// TODO:
}
}
.padding(.vertical, 24)
.padding(.horizontal, 20)
.background(Color.black.opacity(0.3))
.cornerRadius(16)
.padding(.horizontal, 24)
.padding(.top, 32)
//
VStack(spacing: 12) {
//
SettingRowView(
icon: "globe",
title: "语言设置",
action: {
// TODO:
}
)
//
SettingRowView(
icon: "info.circle",
title: "关于我们",
action: {
// TODO:
}
)
//
HStack {
Image(systemName: "app.badge")
.foregroundColor(.white.opacity(0.8))
.frame(width: 24)
Text("版本信息")
.foregroundColor(.white)
Spacer()
Text("1.0.0")
.foregroundColor(.white.opacity(0.6))
.font(.caption)
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
.background(Color.black.opacity(0.2))
.cornerRadius(12)
}
.padding(.horizontal, 24)
)
Spacer(minLength: 50)
// 退
Button(action: {
LogoutButtonView {
store.send(.logoutTapped)
}) {
HStack {
Image(systemName: "arrow.right.square")
Text("退出登录")
}
.font(.body.weight(.medium))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(Color.red.opacity(0.7))
.cornerRadius(12)
}
.padding(.horizontal, 24)
.padding(.bottom, 50)
}
}
@@ -158,6 +78,113 @@ struct SettingView: View {
}
}
// 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")
.font(.system(size: 60))
.foregroundColor(.white.opacity(0.8))
//
VStack(spacing: 8) {
if let userInfo = userInfo, let userName = userInfo.username {
Text(userName)
.font(.title2.weight(.semibold))
.foregroundColor(.white)
} else {
Text(NSLocalizedString("setting.user", comment: "User"))
.font(.title2.weight(.semibold))
.foregroundColor(.white)
}
// ID
if let userInfo = userInfo, let userId = userInfo.userId {
Text("ID: \(userId)")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
} else if let accountModel = accountModel, let uid = accountModel.uid {
Text("UID: \(uid)")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
}
}
}
.padding(.vertical, 24)
.padding(.horizontal, 20)
.background(Color.black.opacity(0.3))
.cornerRadius(16)
.padding(.horizontal, 24)
}
}
// MARK: - Setting Options View
struct SettingOptionsView: View {
let onLanguageTapped: () -> Void
let onAboutTapped: () -> Void
var body: some View {
VStack(spacing: 12) {
SettingRowView(
icon: "globe",
title: NSLocalizedString("setting.language", comment: "Language Settings"),
action: onLanguageTapped
)
SettingRowView(
icon: "info.circle",
title: NSLocalizedString("setting.about", comment: "About Us"),
action: onAboutTapped
)
HStack {
Image(systemName: "app.badge")
.foregroundColor(.white.opacity(0.8))
.frame(width: 24)
Text(NSLocalizedString("setting.version", comment: "Version Info"))
.foregroundColor(.white)
Spacer()
Text("1.0.0")
.foregroundColor(.white.opacity(0.6))
.font(.caption)
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
.background(Color.black.opacity(0.2))
.cornerRadius(12)
}
.padding(.horizontal, 24)
}
}
// MARK: - Logout Button View
struct LogoutButtonView: View {
let action: () -> Void
var body: some View {
Button(action: action) {
HStack {
Image(systemName: "arrow.right.square")
Text(NSLocalizedString("setting.logout", comment: "Logout"))
}
.font(.body.weight(.medium))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(Color.red.opacity(0.7))
.cornerRadius(12)
}
.padding(.horizontal, 24)
}
}
// MARK: - Setting Row View
struct SettingRowView: View {
let icon: String

View File

@@ -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)