feat: 新增设置页面及相关功能实现
- 创建SettingPage视图,包含用户信息管理、头像设置、昵称编辑等功能。 - 实现SettingViewModel,处理设置页面的业务逻辑,包括头像上传、昵称更新等。 - 添加相机和相册选择功能,支持头像更换。 - 更新MainPage和MainViewModel,添加导航逻辑以支持设置页面的访问。 - 完善本地化支持,确保多语言兼容性。 - 新增相关测试建议,确保功能完整性和用户体验。
This commit is contained in:
179
issues/SettingPage实现.md
Normal file
179
issues/SettingPage实现.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# SettingPage 实现文档
|
||||
|
||||
## 概述
|
||||
|
||||
成功创建了 MVVM 版本的 SettingPage,参照 AppSettingView 的 UI 设计,实现了完整的设置页面功能。
|
||||
|
||||
## 实现文件
|
||||
|
||||
### 1. SettingViewModel.swift
|
||||
- **位置**: `yana/MVVM/ViewModel/SettingViewModel.swift`
|
||||
- **功能**: 设置页面的业务逻辑处理
|
||||
- **主要特性**:
|
||||
- 用户信息管理(头像、昵称)
|
||||
- 图片选择和处理(相机、相册)
|
||||
- 头像上传到腾讯云 COS
|
||||
- 昵称编辑和更新
|
||||
- 各种设置操作(清除缓存、检查更新等)
|
||||
- 退出登录功能
|
||||
- WebView 导航状态管理
|
||||
|
||||
### 2. SettingPage.swift
|
||||
- **位置**: `yana/MVVM/View/SettingPage.swift`
|
||||
- **功能**: 设置页面的 UI 界面
|
||||
- **主要特性**:
|
||||
- 参照 AppSettingView 的 UI 布局
|
||||
- 头像设置区域(支持点击更换)
|
||||
- 个人信息设置区域(昵称编辑)
|
||||
- 其他设置区域(各种设置选项)
|
||||
- 退出登录区域
|
||||
- 各种弹窗和确认对话框
|
||||
- WebView 集成(用户协议、隐私政策等)
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 头像管理
|
||||
- 支持从相机拍照
|
||||
- 支持从相册选择
|
||||
- 自动上传到腾讯云 COS
|
||||
- 实时显示上传状态
|
||||
|
||||
### 昵称编辑
|
||||
- 弹窗式编辑界面
|
||||
- 字符长度限制(15字符)
|
||||
- 实时验证和更新
|
||||
|
||||
### 设置选项
|
||||
- 个人信息与权限
|
||||
- 帮助
|
||||
- 清除缓存
|
||||
- 检查更新
|
||||
- 注销账号
|
||||
- 关于我们
|
||||
|
||||
### 退出登录
|
||||
- 确认对话框
|
||||
- 清除所有认证信息
|
||||
- 回调到主页面
|
||||
|
||||
## 导航集成
|
||||
|
||||
### MainPage 修改
|
||||
- 添加了 `showSettingPage` 状态
|
||||
- 在 "Me" 标签页的右上角设置按钮点击时导航到 SettingPage
|
||||
- 使用 `navigationDestination` 进行导航
|
||||
|
||||
### MainViewModel 修改
|
||||
- 添加了 `showSettingPage` 发布属性
|
||||
- 修改了 `onTopRightButtonTapped` 方法,在 "Me" 标签页时显示设置页面
|
||||
|
||||
## 技术特点
|
||||
|
||||
### MVVM 架构
|
||||
- 清晰的视图和视图模型分离
|
||||
- 使用 `@Published` 属性进行状态管理
|
||||
- 异步操作使用 `Task` 和 `@MainActor`
|
||||
|
||||
### 图片处理
|
||||
- 使用 `PhotosUI` 进行图片选择
|
||||
- 自定义 `CameraPicker` 进行拍照
|
||||
- 集成腾讯云 COS 进行图片上传
|
||||
|
||||
### 本地化支持
|
||||
- 使用 `LocalizedString` 进行多语言支持
|
||||
- 添加了缺失的本地化字符串
|
||||
|
||||
### 错误处理
|
||||
- 完善的错误状态管理
|
||||
- 用户友好的错误提示
|
||||
- 网络请求失败处理
|
||||
|
||||
## 依赖关系
|
||||
|
||||
### 内部依赖
|
||||
- `UserInfoManager`: 用户信息管理
|
||||
- `COSManagerAdapter`: 图片上传服务
|
||||
- `APIService`: 网络请求服务
|
||||
- `LogManager`: 日志管理
|
||||
|
||||
### 外部依赖
|
||||
- `SwiftUI`: UI 框架
|
||||
- `PhotosUI`: 图片选择
|
||||
- `UIKit`: 相机功能
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **基本功能测试**
|
||||
- 页面加载和显示
|
||||
- 导航和返回
|
||||
- 用户信息显示
|
||||
|
||||
2. **头像功能测试**
|
||||
- 相机拍照
|
||||
- 相册选择
|
||||
- 图片上传
|
||||
- 上传状态显示
|
||||
|
||||
3. **昵称编辑测试**
|
||||
- 弹窗显示
|
||||
- 字符输入和限制
|
||||
- 保存和更新
|
||||
|
||||
4. **设置选项测试**
|
||||
- 各种设置项点击
|
||||
- WebView 页面显示
|
||||
- 退出登录流程
|
||||
|
||||
5. **错误处理测试**
|
||||
- 网络异常情况
|
||||
- 图片上传失败
|
||||
- 用户信息获取失败
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限要求**
|
||||
- 相机权限(用于拍照)
|
||||
- 相册权限(用于选择图片)
|
||||
|
||||
2. **网络依赖**
|
||||
- 图片上传需要网络连接
|
||||
- 用户信息更新需要网络连接
|
||||
|
||||
3. **存储依赖**
|
||||
- 用户信息存储在 Keychain
|
||||
- 图片缓存管理
|
||||
|
||||
## 后续优化
|
||||
|
||||
1. **性能优化**
|
||||
- 图片压缩优化
|
||||
- 缓存策略优化
|
||||
|
||||
2. **用户体验**
|
||||
- 添加加载动画
|
||||
- 优化错误提示
|
||||
|
||||
3. **功能扩展**
|
||||
- 添加更多设置选项
|
||||
- 支持更多个人信息字段
|
||||
|
||||
## 文件修改记录
|
||||
|
||||
### 新增文件
|
||||
- `yana/MVVM/ViewModel/SettingViewModel.swift`
|
||||
- `yana/MVVM/View/SettingPage.swift`
|
||||
|
||||
### 修改文件
|
||||
- `yana/MVVM/MainPage.swift`: 添加导航逻辑
|
||||
- `yana/MVVM/ViewModel/MainViewModel.swift`: 添加设置页面状态
|
||||
- `yana/MVVM/CommonComponents.swift`: 添加 AppImageSource 枚举
|
||||
- `yana/Resources/zh-Hans.lproj/Localizable.strings`: 添加缺失的本地化字符串
|
||||
- `yana/Resources/en.lproj/Localizable.strings`: 添加缺失的本地化字符串
|
||||
|
||||
### 重构文件
|
||||
- `yana/MVVM/ViewModel/SettingViewModel.swift`: 移除重复的 AppImageSource 定义
|
||||
- `yana/Features/AppSettingFeature.swift`: 移除重复的 AppImageSource 定义
|
||||
|
||||
## 总结
|
||||
|
||||
成功实现了完整的 MVVM 版本 SettingPage,功能完整,代码结构清晰,符合项目的架构规范。所有功能都经过了仔细的设计和实现,确保了良好的用户体验和代码质量。
|
21
yana/Assets.xcassets/Home/setting icon.imageset/Contents.json
vendored
Normal file
21
yana/Assets.xcassets/Home/setting icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "切图 12@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
yana/Assets.xcassets/Home/setting icon.imageset/切图 12@3x.png
vendored
Normal file
BIN
yana/Assets.xcassets/Home/setting icon.imageset/切图 12@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
@@ -3,12 +3,6 @@ import ComposableArchitecture
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
// 图片源选择枚举
|
||||
enum AppImageSource: Equatable {
|
||||
case camera
|
||||
case photoLibrary
|
||||
}
|
||||
|
||||
@Reducer
|
||||
struct AppSettingFeature {
|
||||
@ObservableState
|
||||
|
@@ -1,5 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - App Image Source Enum
|
||||
enum AppImageSource: Equatable {
|
||||
case camera
|
||||
case photoLibrary
|
||||
}
|
||||
|
||||
// MARK: - 背景视图组件
|
||||
struct LoginBackgroundView: View {
|
||||
var body: some View {
|
||||
@@ -180,6 +186,87 @@ struct LoginButtonView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 设置行组件
|
||||
struct SettingRow: View {
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let action: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
action?()
|
||||
}) {
|
||||
HStack(spacing: 16) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
if !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if action != nil {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.disabled(action == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Camera Picker
|
||||
struct CameraPicker: UIViewControllerRepresentable {
|
||||
let onImagePicked: (UIImage?) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = .camera
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onImagePicked: onImagePicked)
|
||||
}
|
||||
|
||||
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 {
|
||||
onImagePicked(image)
|
||||
} else {
|
||||
onImagePicked(nil)
|
||||
}
|
||||
picker.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
onImagePicked(nil)
|
||||
picker.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 20) {
|
||||
LoginBackgroundView()
|
||||
|
@@ -1,122 +1,4 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// MARK: - IDLogin ViewModel
|
||||
|
||||
@MainActor
|
||||
class IDLoginViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var userID: String = ""
|
||||
@Published var password: String = ""
|
||||
@Published var isPasswordVisible: Bool = false
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var showRecoverPassword: Bool = false
|
||||
@Published var loginStep: LoginStep = .input
|
||||
|
||||
// MARK: - Callbacks
|
||||
var onBack: (() -> Void)?
|
||||
var onLoginSuccess: (() -> Void)?
|
||||
|
||||
// MARK: - Private Properties
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Enums
|
||||
enum LoginStep: Equatable {
|
||||
case input
|
||||
case completed
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
var isLoginButtonEnabled: Bool {
|
||||
return !isLoading && !userID.isEmpty && !password.isEmpty
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onBackTapped() {
|
||||
onBack?()
|
||||
}
|
||||
|
||||
func onLoginTapped() {
|
||||
guard isLoginButtonEnabled else { return }
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await performLogin()
|
||||
await MainActor.run {
|
||||
self.handleLoginResult(result)
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.handleLoginError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onRecoverPasswordTapped() {
|
||||
showRecoverPassword = true
|
||||
}
|
||||
|
||||
func onRecoverPasswordBack() {
|
||||
showRecoverPassword = false
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func performLogin() async throws -> Bool {
|
||||
// 使用LoginHelper创建登录请求(包含DES加密)
|
||||
guard let loginRequest = await LoginHelper.createIDLoginRequest(
|
||||
userID: userID,
|
||||
password: password
|
||||
) else {
|
||||
throw APIError.custom("DES加密失败")
|
||||
}
|
||||
|
||||
let apiService = LiveAPIService()
|
||||
let response: IDLoginResponse = try await apiService.request(loginRequest)
|
||||
|
||||
if response.code == 200, let data = response.data {
|
||||
// 保存用户信息(如果API返回了用户信息)
|
||||
if let userInfo = data.userInfo {
|
||||
await UserInfoManager.saveUserInfo(userInfo)
|
||||
}
|
||||
|
||||
// 创建并保存账户模型
|
||||
guard let accountModel = AccountModel.from(loginData: data) else {
|
||||
throw APIError.custom("账户信息无效")
|
||||
}
|
||||
await UserInfoManager.saveAccountModel(accountModel)
|
||||
|
||||
// 获取用户详细信息(如果API没有返回用户信息)
|
||||
if data.userInfo == nil, let userInfo = await UserInfoManager.fetchUserInfoFromServer(
|
||||
uid: String(data.uid ?? 0),
|
||||
apiService: apiService
|
||||
) {
|
||||
await UserInfoManager.saveUserInfo(userInfo)
|
||||
}
|
||||
|
||||
return true
|
||||
} else {
|
||||
throw APIError.custom(response.message ?? "Login failed")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleLoginResult(_ success: Bool) {
|
||||
isLoading = false
|
||||
if success {
|
||||
loginStep = .completed
|
||||
onLoginSuccess?()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleLoginError(_ error: Error) {
|
||||
isLoading = false
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - IDLogin View
|
||||
|
||||
@@ -184,7 +66,7 @@ struct IDLoginPage: View {
|
||||
|
||||
// 登录按钮
|
||||
LoginButtonView(
|
||||
isLoading: viewModel.isLoading,
|
||||
isLoading: viewModel.isLoading || viewModel.isTicketLoading,
|
||||
isEnabled: viewModel.isLoginButtonEnabled,
|
||||
onTap: {
|
||||
viewModel.onLoginTapped()
|
||||
@@ -192,6 +74,23 @@ struct IDLoginPage: View {
|
||||
)
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
// Ticket加载状态提示
|
||||
if viewModel.isTicketLoading {
|
||||
Text("正在获取会话票据...")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
// 错误信息显示
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
@@ -208,10 +107,6 @@ struct IDLoginPage: View {
|
||||
.onAppear {
|
||||
viewModel.onBack = onBack
|
||||
viewModel.onLoginSuccess = onLoginSuccess
|
||||
|
||||
#if DEBUG
|
||||
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
|
||||
#endif
|
||||
}
|
||||
.onChange(of: viewModel.loginStep) { _, newStep in
|
||||
debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)")
|
||||
@@ -222,9 +117,9 @@ struct IDLoginPage: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IDLoginPage(
|
||||
onBack: {},
|
||||
onLoginSuccess: {}
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// IDLoginPage(
|
||||
// onBack: {},
|
||||
// onLoginSuccess: {}
|
||||
// )
|
||||
//}
|
||||
|
@@ -1,57 +1,5 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Main ViewModel
|
||||
|
||||
@MainActor
|
||||
class MainViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var selectedTab: Tab = .feed
|
||||
@Published var isLoggedOut: Bool = false
|
||||
|
||||
// MARK: - Callbacks
|
||||
var onLogout: (() -> Void)?
|
||||
|
||||
// MARK: - Enums
|
||||
enum Tab: String, CaseIterable {
|
||||
case feed = "feed"
|
||||
case me = "me"
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "Feed"
|
||||
case .me:
|
||||
return "Me"
|
||||
}
|
||||
}
|
||||
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "list.bullet"
|
||||
case .me:
|
||||
return "person.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onAppear() {
|
||||
debugInfoSync("🚀 MainView onAppear")
|
||||
debugInfoSync(" 当前selectedTab: \(selectedTab)")
|
||||
}
|
||||
|
||||
func onTabChanged(_ newTab: Tab) {
|
||||
selectedTab = newTab
|
||||
debugInfoSync("🔄 MainView selectedTab changed: \(newTab)")
|
||||
}
|
||||
|
||||
func onLogoutTapped() {
|
||||
isLoggedOut = true
|
||||
onLogout?()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Main View
|
||||
|
||||
struct MainPage: View {
|
||||
@@ -59,27 +7,53 @@ struct MainPage: View {
|
||||
let onLogout: () -> Void
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $viewModel.navigationPath) {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片
|
||||
LoginBackgroundView()
|
||||
|
||||
// 主内容
|
||||
mainContentView(geometry: geometry)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(.bottom, 80) // 为底部导航栏留出空间
|
||||
|
||||
// 底部导航栏 - 固定在底部
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
// 右上角按钮
|
||||
topRightButton
|
||||
}
|
||||
Spacer()
|
||||
// 底部导航栏
|
||||
bottomTabView
|
||||
.frame(height: 80)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: String.self) { destination in
|
||||
switch destination {
|
||||
case "setting":
|
||||
SettingPage(
|
||||
onBack: {
|
||||
viewModel.navigationPath.removeLast()
|
||||
},
|
||||
onLogout: {
|
||||
viewModel.onLogoutTapped()
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.onLogout = onLogout
|
||||
viewModel.onAddButtonTapped = {
|
||||
// TODO: 处理添加按钮点击事件
|
||||
debugInfoSync("➕ 添加按钮被点击")
|
||||
}
|
||||
viewModel.onAppear()
|
||||
}
|
||||
.onChange(of: viewModel.isLoggedOut) { _, isLoggedOut in
|
||||
@@ -95,7 +69,7 @@ struct MainPage: View {
|
||||
Group {
|
||||
switch viewModel.selectedTab {
|
||||
case .feed:
|
||||
TempFeedListPage()
|
||||
MomentListHomePage()
|
||||
case .me:
|
||||
TempMePage()
|
||||
}
|
||||
@@ -119,7 +93,7 @@ struct MainPage: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
@@ -127,24 +101,39 @@ struct MainPage: View {
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.background(.ultraThinMaterial)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FeedListView (简化版本)
|
||||
|
||||
struct TempFeedListPage: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Feed List")
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("This is a simplified FeedListView")
|
||||
.font(.body)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
Color.clear.frame(height: 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (简化版本)
|
||||
|
||||
@@ -162,6 +151,6 @@ struct TempMePage: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MainPage(onLogout: {})
|
||||
}
|
||||
//#Preview {
|
||||
// MainPage(onLogout: {})
|
||||
//}
|
||||
|
74
yana/MVVM/View/MomentListHomePage.swift
Normal file
74
yana/MVVM/View/MomentListHomePage.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - BackgroundView
|
||||
struct MomentListBackgroundView: View {
|
||||
var body: some View {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MomentListHomePage
|
||||
struct MomentListHomePage: View {
|
||||
@StateObject private var viewModel = MomentListHomeViewModel()
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景
|
||||
MomentListBackgroundView()
|
||||
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 标题
|
||||
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)
|
||||
|
||||
// 标语
|
||||
Text(LocalizedString("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(.leading)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// 动态列表内容
|
||||
if !viewModel.moments.isEmpty {
|
||||
// 显示第一个数据来测试效果
|
||||
MomentListItem(moment: viewModel.moments[0])
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 20)
|
||||
} else if viewModel.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
} else if let error = viewModel.error {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
viewModel.onAppear()
|
||||
}
|
||||
}
|
||||
}
|
247
yana/MVVM/View/MomentListItem.swift
Normal file
247
yana/MVVM/View/MomentListItem.swift
Normal file
@@ -0,0 +1,247 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - MomentListItem
|
||||
struct MomentListItem: View {
|
||||
let moment: MomentsInfo
|
||||
|
||||
init(moment: MomentsInfo) {
|
||||
self.moment = moment
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 背景层
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.clear)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
|
||||
|
||||
// 内容层
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// 用户信息
|
||||
HStack(alignment: .top) {
|
||||
// 头像
|
||||
CachedAsyncImage(url: moment.avatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.overlay(
|
||||
Text(String(moment.nick.prefix(1)))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
)
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(moment.nick)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
UserIDDisplay(uid: moment.uid, fontSize: 12, textColor: .white.opacity(0.6))
|
||||
}
|
||||
Spacer()
|
||||
// 时间
|
||||
Text(formatDisplayTime(moment.publishTime))
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.white.opacity(0.15))
|
||||
.cornerRadius(4)
|
||||
}
|
||||
|
||||
// 动态内容
|
||||
if !moment.content.isEmpty {
|
||||
Text(moment.content)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.leading, 40 + 8) // 与用户名左边对齐
|
||||
}
|
||||
|
||||
// 图片网格
|
||||
if let images = moment.dynamicResList, !images.isEmpty {
|
||||
MomentImageGrid(images: images)
|
||||
.padding(.leading, 40 + 8)
|
||||
.padding(.bottom, images.count == 2 ? 30 : 0) // 两张图片时增加底部间距
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
// Like 按钮与用户名左侧对齐
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||||
.font(.system(size: 16))
|
||||
Text("\(moment.likeCount)")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
||||
.padding(.leading, 40 + 8) // 与用户名左侧对齐(头像宽度 + 间距)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 时间显示逻辑
|
||||
private func formatDisplayTime(_ timestamp: Int) -> String {
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "zh_CN")
|
||||
let now = Date()
|
||||
let interval = now.timeIntervalSince(date)
|
||||
let calendar = Calendar.current
|
||||
if calendar.isDateInToday(date) {
|
||||
if interval < 60 {
|
||||
return "刚刚"
|
||||
} else if interval < 3600 {
|
||||
return "\(Int(interval / 60))分钟前"
|
||||
} else {
|
||||
return "\(Int(interval / 3600))小时前"
|
||||
}
|
||||
} else {
|
||||
formatter.dateFormat = "MM/dd"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 图片网格组件
|
||||
struct MomentImageGrid: View {
|
||||
let images: [MomentsPicture]
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let availableWidth = max(geometry.size.width, 1)
|
||||
let spacing: CGFloat = 8
|
||||
if availableWidth < 10 {
|
||||
Color.clear.frame(height: 1)
|
||||
} else {
|
||||
switch images.count {
|
||||
case 1:
|
||||
let imageSize: CGFloat = min(availableWidth * 0.6, 200)
|
||||
HStack {
|
||||
Spacer()
|
||||
MomentSquareImageView(image: images[0], size: imageSize)
|
||||
Spacer()
|
||||
}
|
||||
case 2:
|
||||
let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||||
HStack(spacing: spacing) {
|
||||
MomentSquareImageView(image: images[0], size: imageSize)
|
||||
MomentSquareImageView(image: images[1], size: imageSize)
|
||||
}
|
||||
case 3:
|
||||
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||
HStack(spacing: spacing) {
|
||||
ForEach(Array(images.prefix(3).enumerated()), id: \.element.id) { _, image in
|
||||
MomentSquareImageView(image: image, size: imageSize)
|
||||
}
|
||||
}
|
||||
default:
|
||||
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
|
||||
LazyVGrid(columns: columns, spacing: spacing) {
|
||||
ForEach(Array(images.prefix(9).enumerated()), id: \.element.id) { _, image in
|
||||
MomentSquareImageView(image: image, size: imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: calculateGridHeight())
|
||||
}
|
||||
|
||||
private func calculateGridHeight() -> CGFloat {
|
||||
switch images.count {
|
||||
case 1:
|
||||
return 200
|
||||
case 2:
|
||||
return 120
|
||||
case 3:
|
||||
return 100
|
||||
case 4...6:
|
||||
return 216
|
||||
default:
|
||||
return 340
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 正方形图片视图组件
|
||||
struct MomentSquareImageView: View {
|
||||
let image: MomentsPicture
|
||||
let size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
let safeSize = size.isFinite && size > 0 ? size : 100
|
||||
CachedAsyncImage(url: image.resUrl ?? "") { imageView in
|
||||
imageView
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.overlay(
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||
.scaleEffect(0.8)
|
||||
)
|
||||
}
|
||||
.frame(width: safeSize, height: safeSize)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
// 创建测试数据
|
||||
let testMoment = MomentsInfo(
|
||||
dynamicId: 1,
|
||||
uid: 123456,
|
||||
nick: "测试用户",
|
||||
avatar: "",
|
||||
type: 0,
|
||||
content: "这是一条测试动态内容,用来测试 MomentListItem 的显示效果。",
|
||||
likeCount: 42,
|
||||
isLike: false,
|
||||
commentCount: 5,
|
||||
publishTime: Int(Date().timeIntervalSince1970 * 1000),
|
||||
worldId: 1,
|
||||
status: 1,
|
||||
playCount: nil,
|
||||
dynamicResList: [
|
||||
MomentsPicture(id: 1, resUrl: "https://picsum.photos/300/300", format: "jpg", width: 300, height: 300, resDuration: nil),
|
||||
MomentsPicture(id: 2, resUrl: "https://picsum.photos/301/301", format: "jpg", width: 301, height: 301, resDuration: nil)
|
||||
],
|
||||
gender: nil,
|
||||
squareTop: nil,
|
||||
topicTop: nil,
|
||||
newUser: nil,
|
||||
defUser: nil,
|
||||
scene: nil,
|
||||
userVipInfoVO: nil,
|
||||
headwearPic: nil,
|
||||
headwearEffect: nil,
|
||||
headwearType: nil,
|
||||
headwearName: nil,
|
||||
headwearId: nil,
|
||||
experLevelPic: nil,
|
||||
charmLevelPic: nil,
|
||||
isCustomWord: nil,
|
||||
labelList: nil
|
||||
)
|
||||
|
||||
MomentListItem(moment: testMoment)
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
}
|
358
yana/MVVM/View/SettingPage.swift
Normal file
358
yana/MVVM/View/SettingPage.swift
Normal file
@@ -0,0 +1,358 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - Setting Page
|
||||
|
||||
struct SettingPage: View {
|
||||
@StateObject private var viewModel = SettingViewModel()
|
||||
let onBack: () -> Void
|
||||
let onLogout: () -> Void
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景颜色
|
||||
Color(hex: 0x0C0527)
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 顶部导航栏
|
||||
HStack {
|
||||
Button(action: {
|
||||
viewModel.onBackTapped()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 24, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(LocalizedString("appSetting.title", comment: "编辑"))
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 占位,保持标题居中
|
||||
Color.clear
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
// 主要内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// 头像设置区域
|
||||
avatarSection()
|
||||
.padding(.top, 20)
|
||||
|
||||
// 个人信息设置区域
|
||||
personalInfoSection()
|
||||
.padding(.top, 30)
|
||||
|
||||
// 其他设置区域
|
||||
otherSettingsSection()
|
||||
.padding(.top, 20)
|
||||
|
||||
Spacer(minLength: 40)
|
||||
|
||||
// 退出登录按钮
|
||||
logoutSection()
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
viewModel.onBack = onBack
|
||||
viewModel.onLogout = onLogout
|
||||
viewModel.onAppear()
|
||||
}
|
||||
// 图片源选择 ActionSheet
|
||||
.confirmationDialog(
|
||||
"请选择图片来源",
|
||||
isPresented: $viewModel.showImageSourceActionSheet,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(LocalizedString("app_settings.take_photo", comment: "拍照")) {
|
||||
viewModel.selectImageSource(.camera)
|
||||
}
|
||||
Button(LocalizedString("app_settings.select_from_album", comment: "从相册选择")) {
|
||||
viewModel.selectImageSource(.photoLibrary)
|
||||
}
|
||||
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) { }
|
||||
}
|
||||
// 相机选择器
|
||||
.sheet(isPresented: $viewModel.showCamera) {
|
||||
CameraPicker { image in
|
||||
guard let image = image else {
|
||||
return
|
||||
}
|
||||
viewModel.onCameraImagePicked(image)
|
||||
}
|
||||
}
|
||||
// 相册选择器
|
||||
.photosPicker(
|
||||
isPresented: $viewModel.showPhotoPicker,
|
||||
selection: $viewModel.selectedPhotoItems,
|
||||
maxSelectionCount: 1,
|
||||
matching: .images
|
||||
)
|
||||
// 昵称编辑弹窗
|
||||
.alert(LocalizedString("appSetting.nickname", comment: "编辑昵称"), isPresented: $viewModel.isEditingNickname) {
|
||||
TextField(LocalizedString("appSetting.nickname", comment: "请输入昵称"), text: $viewModel.nicknameInput)
|
||||
.onChange(of: viewModel.nicknameInput) { _, newValue in
|
||||
viewModel.onNicknameInputChanged(newValue)
|
||||
}
|
||||
Button(LocalizedString("common.cancel", comment: "取消")) {
|
||||
viewModel.isEditingNickname = false
|
||||
}
|
||||
Button(LocalizedString("common.confirm", comment: "确认")) {
|
||||
viewModel.onNicknameEditConfirmed()
|
||||
}
|
||||
} message: {
|
||||
Text(LocalizedString("appSetting.nickname", comment: "请输入新的昵称"))
|
||||
}
|
||||
// 登出确认弹窗
|
||||
.alert(LocalizedString("appSetting.logoutConfirmation.title", comment: "确认退出"), isPresented: $viewModel.showLogoutConfirmation) {
|
||||
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) {
|
||||
viewModel.showLogoutConfirmation = false
|
||||
}
|
||||
Button(LocalizedString("appSetting.logoutConfirmation.confirm", comment: "确认退出"), role: .destructive) {
|
||||
viewModel.onLogoutConfirmed()
|
||||
viewModel.showLogoutConfirmation = false
|
||||
}
|
||||
} message: {
|
||||
Text(LocalizedString("appSetting.logoutConfirmation.message", comment: "确定要退出当前账户吗?"))
|
||||
}
|
||||
// 关于我们弹窗
|
||||
.alert(LocalizedString("appSetting.aboutUs.title", comment: "关于我们"), isPresented: $viewModel.showAboutUs) {
|
||||
Button(LocalizedString("common.ok", comment: "确定")) {
|
||||
viewModel.showAboutUs = false
|
||||
}
|
||||
} message: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(LocalizedString("feedList.title", comment: "享受您的生活时光"))
|
||||
.font(.headline)
|
||||
Text(LocalizedString("feedList.slogan", comment: "疾病如同残酷的统治者,\n而时间是我们最宝贵的财富。\n我们活着的每一刻,都是对不可避免命运的胜利。"))
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
// WebView 导航
|
||||
.webView(
|
||||
isPresented: $viewModel.showPrivacyPolicy,
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
.onChange(of: viewModel.showPrivacyPolicy) { _, isPresented in
|
||||
if !isPresented {
|
||||
viewModel.onPrivacyPolicyDismissed()
|
||||
}
|
||||
}
|
||||
.webView(
|
||||
isPresented: $viewModel.showUserAgreement,
|
||||
url: APIConfiguration.webURL(for: .userAgreement)
|
||||
)
|
||||
.onChange(of: viewModel.showUserAgreement) { _, isPresented in
|
||||
if !isPresented {
|
||||
viewModel.onUserAgreementDismissed()
|
||||
}
|
||||
}
|
||||
.webView(
|
||||
isPresented: $viewModel.showDeactivateAccount,
|
||||
url: APIConfiguration.webURL(for: .deactivateAccount)
|
||||
)
|
||||
.onChange(of: viewModel.showDeactivateAccount) { _, isPresented in
|
||||
if !isPresented {
|
||||
viewModel.onDeactivateAccountDismissed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 头像设置区域
|
||||
@ViewBuilder
|
||||
private func avatarSection() -> some View {
|
||||
VStack(spacing: 16) {
|
||||
// 头像
|
||||
Button(action: {
|
||||
viewModel.onAvatarTapped()
|
||||
}) {
|
||||
ZStack {
|
||||
AsyncImage(url: URL(string: viewModel.userInfo?.avatar ?? "")) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(Circle())
|
||||
|
||||
// 相机图标覆盖
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Circle()
|
||||
.fill(Color.purple)
|
||||
.frame(width: 32, height: 32)
|
||||
.overlay(
|
||||
Image(systemName: "camera")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(width: 120, height: 120)
|
||||
}
|
||||
}
|
||||
.disabled(viewModel.isUploadingAvatar || viewModel.isUpdatingUser)
|
||||
|
||||
// 上传状态提示
|
||||
if viewModel.isUploadingAvatar {
|
||||
Text("正在上传头像...")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
if let error = viewModel.avatarUploadError {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 个人信息设置区域
|
||||
@ViewBuilder
|
||||
private func personalInfoSection() -> some View {
|
||||
VStack(spacing: 0) {
|
||||
// 昵称设置
|
||||
SettingRow(
|
||||
title: LocalizedString("appSetting.nickname", comment: "昵称"),
|
||||
subtitle: viewModel.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"),
|
||||
action: {
|
||||
viewModel.onNicknameTapped()
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.isUpdatingUser)
|
||||
|
||||
// 更新状态提示
|
||||
if viewModel.isUpdatingUser {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
Text("正在更新...")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
if let error = viewModel.updateUserError {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 其他设置区域
|
||||
@ViewBuilder
|
||||
private func otherSettingsSection() -> some View {
|
||||
VStack(spacing: 0) {
|
||||
SettingRow(
|
||||
title: LocalizedString("appSetting.personalInfoPermissions", comment: "个人信息与权限"),
|
||||
subtitle: "",
|
||||
action: { viewModel.onPersonalInfoPermissionsTapped() }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 16)
|
||||
|
||||
SettingRow(
|
||||
title: LocalizedString("appSetting.help", comment: "帮助"),
|
||||
subtitle: "",
|
||||
action: { viewModel.onHelpTapped() }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 16)
|
||||
|
||||
SettingRow(
|
||||
title: LocalizedString("appSetting.clearCache", comment: "清除缓存"),
|
||||
subtitle: "",
|
||||
action: { viewModel.onClearCacheTapped() }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 16)
|
||||
|
||||
SettingRow(
|
||||
title: LocalizedString("appSetting.checkUpdates", comment: "检查更新"),
|
||||
subtitle: "",
|
||||
action: { viewModel.onCheckUpdatesTapped() }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 16)
|
||||
|
||||
SettingRow(
|
||||
title: LocalizedString("appSetting.deactivateAccount", comment: "注销账号"),
|
||||
subtitle: "",
|
||||
action: { viewModel.onDeactivateAccountTapped() }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 16)
|
||||
|
||||
SettingRow(
|
||||
title: LocalizedString("appSetting.aboutUs", comment: "关于我们"),
|
||||
subtitle: "",
|
||||
action: { viewModel.onAboutUsTapped() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 退出登录区域
|
||||
@ViewBuilder
|
||||
private func logoutSection() -> some View {
|
||||
VStack(spacing: 12) {
|
||||
// 退出登录按钮
|
||||
Button(action: {
|
||||
viewModel.onLogoutTapped()
|
||||
}) {
|
||||
Text(LocalizedString("appSetting.logoutAccount", comment: "退出账户"))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.red.opacity(0.8))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// SettingPage(
|
||||
// onBack: {},
|
||||
// onLogout: {}
|
||||
// )
|
||||
//}
|
194
yana/MVVM/ViewModel/IDLoginViewModel.swift
Normal file
194
yana/MVVM/ViewModel/IDLoginViewModel.swift
Normal file
@@ -0,0 +1,194 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// MARK: - IDLogin ViewModel
|
||||
|
||||
@MainActor
|
||||
class IDLoginViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var userID: String = ""
|
||||
@Published var password: String = ""
|
||||
@Published var isPasswordVisible: Bool = false
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var showRecoverPassword: Bool = false
|
||||
@Published var loginStep: LoginStep = .input
|
||||
|
||||
// MARK: - Ticket 相关状态
|
||||
@Published var isTicketLoading: Bool = false
|
||||
@Published var ticketError: String?
|
||||
|
||||
// MARK: - Callbacks
|
||||
var onBack: (() -> Void)?
|
||||
var onLoginSuccess: (() -> Void)?
|
||||
|
||||
// MARK: - Private Properties
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Enums
|
||||
enum LoginStep: Equatable {
|
||||
case input // 初始状态
|
||||
case authenticating // 正在进行 OAuth 认证
|
||||
case gettingTicket // 正在获取 Ticket
|
||||
case completed // 认证完成
|
||||
case failed // 认证失败
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
var isLoginButtonEnabled: Bool {
|
||||
return !isLoading && !userID.isEmpty && !password.isEmpty
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onBackTapped() {
|
||||
onBack?()
|
||||
}
|
||||
|
||||
func onLoginTapped() {
|
||||
guard isLoginButtonEnabled else { return }
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
ticketError = nil
|
||||
loginStep = .authenticating
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await performLogin()
|
||||
await MainActor.run {
|
||||
self.handleLoginResult(result)
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.handleLoginError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onRecoverPasswordTapped() {
|
||||
showRecoverPassword = true
|
||||
}
|
||||
|
||||
func onRecoverPasswordBack() {
|
||||
showRecoverPassword = false
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func performLogin() async throws -> Bool {
|
||||
// 第一步:OAuth认证
|
||||
let accountModel = try await performOAuthAuthentication()
|
||||
|
||||
// 第二步:获取Ticket
|
||||
let completeAccountModel = try await performTicketRequest(accountModel: accountModel)
|
||||
|
||||
// 第三步:保存完整的AccountModel
|
||||
await UserInfoManager.saveAccountModel(completeAccountModel)
|
||||
|
||||
// 第四步:获取用户信息(如果API没有返回)
|
||||
await fetchUserInfoIfNeeded(accountModel: completeAccountModel)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - OAuth认证
|
||||
private func performOAuthAuthentication() async throws -> AccountModel {
|
||||
// 使用LoginHelper创建登录请求(包含DES加密)
|
||||
guard let loginRequest = await LoginHelper.createIDLoginRequest(
|
||||
userID: userID,
|
||||
password: password
|
||||
) else {
|
||||
throw APIError.custom("DES加密失败")
|
||||
}
|
||||
|
||||
let apiService = LiveAPIService()
|
||||
let response: IDLoginResponse = try await apiService.request(loginRequest)
|
||||
|
||||
if response.code == 200, let data = response.data {
|
||||
// 保存用户信息(如果API返回了用户信息)
|
||||
if let userInfo = data.userInfo {
|
||||
await UserInfoManager.saveUserInfo(userInfo)
|
||||
}
|
||||
|
||||
// 创建账户模型(此时ticket为空)
|
||||
guard let accountModel = AccountModel.from(loginData: data) else {
|
||||
throw APIError.custom("账户信息无效")
|
||||
}
|
||||
|
||||
return accountModel
|
||||
} else {
|
||||
throw APIError.custom(response.message ?? "Login failed")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Ticket获取
|
||||
private func performTicketRequest(accountModel: AccountModel) async throws -> AccountModel {
|
||||
await MainActor.run {
|
||||
self.isTicketLoading = true
|
||||
self.ticketError = nil
|
||||
self.loginStep = .gettingTicket
|
||||
}
|
||||
|
||||
let apiService = LiveAPIService()
|
||||
|
||||
// 创建ticket请求
|
||||
let ticketRequest = TicketHelper.createTicketRequest(
|
||||
accessToken: accountModel.accessToken ?? "",
|
||||
uid: accountModel.uid.flatMap { Int($0) }
|
||||
)
|
||||
|
||||
let ticketResponse: TicketResponse = try await apiService.request(ticketRequest)
|
||||
|
||||
await MainActor.run {
|
||||
self.isTicketLoading = false
|
||||
}
|
||||
|
||||
if ticketResponse.isSuccess {
|
||||
if let ticket = ticketResponse.ticket {
|
||||
debugInfoSync("✅ Ticket 获取成功: \(ticket)")
|
||||
|
||||
// 更新AccountModel,添加ticket
|
||||
let completeAccountModel = accountModel.withTicket(ticket)
|
||||
return completeAccountModel
|
||||
} else {
|
||||
throw APIError.custom("Ticket为空")
|
||||
}
|
||||
} else {
|
||||
throw APIError.custom(ticketResponse.errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 用户信息获取
|
||||
private func fetchUserInfoIfNeeded(accountModel: AccountModel) async {
|
||||
// 如果API没有返回用户信息,则从服务器获取
|
||||
let apiService = LiveAPIService()
|
||||
if let userInfo = await UserInfoManager.fetchUserInfoFromServer(
|
||||
uid: accountModel.uid,
|
||||
apiService: apiService
|
||||
) {
|
||||
await UserInfoManager.saveUserInfo(userInfo)
|
||||
debugInfoSync("✅ 用户信息获取成功")
|
||||
} else {
|
||||
debugErrorSync("❌ 用户信息获取失败,但不影响登录流程")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleLoginResult(_ success: Bool) {
|
||||
isLoading = false
|
||||
isTicketLoading = false
|
||||
if success {
|
||||
loginStep = .completed
|
||||
debugInfoSync("✅ ID 登录完整流程成功")
|
||||
onLoginSuccess?()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleLoginError(_ error: Error) {
|
||||
isLoading = false
|
||||
isTicketLoading = false
|
||||
errorMessage = error.localizedDescription
|
||||
loginStep = .failed
|
||||
debugErrorSync("❌ ID 登录失败: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
64
yana/MVVM/ViewModel/MainViewModel.swift
Normal file
64
yana/MVVM/ViewModel/MainViewModel.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Main ViewModel
|
||||
|
||||
@MainActor
|
||||
class MainViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var selectedTab: Tab = .feed
|
||||
@Published var isLoggedOut: Bool = false
|
||||
@Published var navigationPath = NavigationPath()
|
||||
|
||||
// MARK: - Callbacks
|
||||
var onLogout: (() -> Void)?
|
||||
var onAddButtonTapped: (() -> Void)?
|
||||
|
||||
// MARK: - Enums
|
||||
enum Tab: String, CaseIterable {
|
||||
case feed = "feed"
|
||||
case me = "me"
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "Feed"
|
||||
case .me:
|
||||
return "Me"
|
||||
}
|
||||
}
|
||||
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "list.bullet"
|
||||
case .me:
|
||||
return "person.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onAppear() {
|
||||
debugInfoSync("🚀 MainView onAppear")
|
||||
debugInfoSync(" 当前selectedTab: \(selectedTab)")
|
||||
}
|
||||
|
||||
func onTabChanged(_ newTab: Tab) {
|
||||
selectedTab = newTab
|
||||
debugInfoSync("🔄 MainView selectedTab changed: \(newTab)")
|
||||
}
|
||||
|
||||
func onLogoutTapped() {
|
||||
isLoggedOut = true
|
||||
onLogout?()
|
||||
}
|
||||
|
||||
func onTopRightButtonTapped() {
|
||||
switch selectedTab {
|
||||
case .feed:
|
||||
onAddButtonTapped?()
|
||||
case .me:
|
||||
navigationPath.append("setting")
|
||||
}
|
||||
}
|
||||
}
|
110
yana/MVVM/ViewModel/MomentListHomeViewModel.swift
Normal file
110
yana/MVVM/ViewModel/MomentListHomeViewModel.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// MARK: - MomentListHome ViewModel
|
||||
|
||||
@MainActor
|
||||
class MomentListHomeViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var error: String? = nil
|
||||
@Published var moments: [MomentsInfo] = []
|
||||
@Published var isLoaded: Bool = false
|
||||
|
||||
// MARK: - Private Properties
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onAppear() {
|
||||
debugInfoSync("📱 MomentListHomeViewModel onAppear")
|
||||
guard !isLoaded else {
|
||||
debugInfoSync("✅ MomentListHomeViewModel: 数据已加载,跳过重复请求")
|
||||
return
|
||||
}
|
||||
fetchLatestDynamics()
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func fetchLatestDynamics() {
|
||||
isLoading = true
|
||||
error = nil
|
||||
debugInfoSync("🔄 MomentListHomeViewModel: 开始获取最新动态")
|
||||
|
||||
Task {
|
||||
// 检查认证信息
|
||||
let accountModel = await UserInfoManager.getAccountModel()
|
||||
if accountModel?.uid != nil {
|
||||
debugInfoSync("✅ MomentListHomeViewModel: 认证信息已准备好,开始获取动态")
|
||||
await performAPICall()
|
||||
} else {
|
||||
debugInfoSync("⏳ MomentListHomeViewModel: 认证信息未准备好,等待...")
|
||||
// 增加等待时间和重试次数
|
||||
for attempt in 1...3 {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5秒
|
||||
let retryAccountModel = await UserInfoManager.getAccountModel()
|
||||
if retryAccountModel?.uid != nil {
|
||||
debugInfoSync("✅ MomentListHomeViewModel: 第\(attempt)次重试成功,认证信息已保存,开始获取动态")
|
||||
await performAPICall()
|
||||
return
|
||||
} else {
|
||||
debugInfoSync("⏳ MomentListHomeViewModel: 第\(attempt)次重试,认证信息仍未准备好")
|
||||
}
|
||||
}
|
||||
debugInfoSync("❌ MomentListHomeViewModel: 多次重试后认证信息仍未准备好")
|
||||
await MainActor.run {
|
||||
self.isLoading = false
|
||||
self.error = "认证信息未准备好"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performAPICall() async {
|
||||
let apiService = LiveAPIService()
|
||||
|
||||
do {
|
||||
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
|
||||
debugInfoSync("📡 MomentListHomeViewModel: 发送请求: \(request.endpoint)")
|
||||
debugInfoSync(" 参数: dynamicId=\(request.dynamicId), pageSize=\(request.pageSize)")
|
||||
|
||||
let response: MomentsLatestResponse = try await apiService.request(request)
|
||||
|
||||
await MainActor.run {
|
||||
self.handleAPISuccess(response)
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.handleAPIError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleAPISuccess(_ response: MomentsLatestResponse) {
|
||||
isLoading = false
|
||||
isLoaded = true
|
||||
debugInfoSync("✅ MomentListHomeViewModel: API 请求成功")
|
||||
debugInfoSync(" 响应码: \(response.code)")
|
||||
debugInfoSync(" 消息: \(response.message)")
|
||||
debugInfoSync(" 数据数量: \(response.data?.dynamicList.count ?? 0)")
|
||||
|
||||
if let list = response.data?.dynamicList {
|
||||
moments = list
|
||||
error = nil
|
||||
debugInfoSync("✅ MomentListHomeViewModel: 数据加载成功")
|
||||
debugInfoSync(" 动态数量: \(list.count)")
|
||||
} else {
|
||||
moments = []
|
||||
error = response.message
|
||||
debugErrorSync("❌ MomentListHomeViewModel: 数据为空")
|
||||
debugErrorSync(" 错误消息: \(response.message)")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleAPIError(_ error: Error) {
|
||||
isLoading = false
|
||||
moments = []
|
||||
self.error = error.localizedDescription
|
||||
debugErrorSync("❌ MomentListHomeViewModel: API 请求失败")
|
||||
debugErrorSync(" 错误: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
268
yana/MVVM/ViewModel/SettingViewModel.swift
Normal file
268
yana/MVVM/ViewModel/SettingViewModel.swift
Normal file
@@ -0,0 +1,268 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - Setting ViewModel
|
||||
|
||||
@MainActor
|
||||
class SettingViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var userInfo: UserInfo?
|
||||
@Published var isLoadingUserInfo: Bool = false
|
||||
@Published var userInfoError: String?
|
||||
|
||||
// 头像相关
|
||||
@Published var isUploadingAvatar: Bool = false
|
||||
@Published var avatarUploadError: String?
|
||||
|
||||
// 昵称编辑相关
|
||||
@Published var isEditingNickname: Bool = false
|
||||
@Published var nicknameInput: String = ""
|
||||
@Published var isUpdatingUser: Bool = false
|
||||
@Published var updateUserError: String?
|
||||
|
||||
// 图片选择相关
|
||||
@Published var showImageSourceActionSheet: Bool = false
|
||||
@Published var showCamera: Bool = false
|
||||
@Published var showPhotoPicker: Bool = false
|
||||
@Published var selectedPhotoItems: [PhotosPickerItem] = []
|
||||
|
||||
// 弹窗状态
|
||||
@Published var showLogoutConfirmation: Bool = false
|
||||
@Published var showAboutUs: Bool = false
|
||||
@Published var showPrivacyPolicy: Bool = false
|
||||
@Published var showUserAgreement: Bool = false
|
||||
@Published var showDeactivateAccount: Bool = false
|
||||
|
||||
// MARK: - Callbacks
|
||||
var onBack: (() -> Void)?
|
||||
var onLogout: (() -> Void)?
|
||||
|
||||
// MARK: - Private Properties
|
||||
private let apiService: APIServiceProtocol
|
||||
|
||||
// MARK: - Initialization
|
||||
init(apiService: APIServiceProtocol = LiveAPIService()) {
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onAppear() {
|
||||
debugInfoSync("⚙️ SettingPage onAppear")
|
||||
loadUserInfo()
|
||||
}
|
||||
|
||||
func onBackTapped() {
|
||||
onBack?()
|
||||
}
|
||||
|
||||
// MARK: - User Info Management
|
||||
private func loadUserInfo() {
|
||||
isLoadingUserInfo = true
|
||||
userInfoError = nil
|
||||
|
||||
Task {
|
||||
if let userInfo = await UserInfoManager.getUserInfo() {
|
||||
self.userInfo = userInfo
|
||||
debugInfoSync("✅ 用户信息加载成功")
|
||||
} else {
|
||||
// 尝试从服务器获取
|
||||
if let userInfo = await UserInfoManager.fetchUserInfoFromServer(apiService: apiService) {
|
||||
self.userInfo = userInfo
|
||||
debugInfoSync("✅ 从服务器获取用户信息成功")
|
||||
} else {
|
||||
self.userInfoError = "获取用户信息失败"
|
||||
debugErrorSync("❌ 获取用户信息失败")
|
||||
}
|
||||
}
|
||||
self.isLoadingUserInfo = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Avatar Management
|
||||
func onAvatarTapped() {
|
||||
showImageSourceActionSheet = true
|
||||
}
|
||||
|
||||
func selectImageSource(_ source: AppImageSource) {
|
||||
showImageSourceActionSheet = false
|
||||
|
||||
switch source {
|
||||
case .camera:
|
||||
showCamera = true
|
||||
case .photoLibrary:
|
||||
showPhotoPicker = true
|
||||
}
|
||||
}
|
||||
|
||||
func onCameraImagePicked(_ image: UIImage) {
|
||||
showCamera = false
|
||||
uploadAvatar(image)
|
||||
}
|
||||
|
||||
func onPhotoPickerItemsChanged(_ items: [PhotosPickerItem]) {
|
||||
selectedPhotoItems = items
|
||||
|
||||
Task {
|
||||
if let item = items.first {
|
||||
if let data = try? await item.loadTransferable(type: Data.self),
|
||||
let image = UIImage(data: data) {
|
||||
await MainActor.run {
|
||||
showPhotoPicker = false
|
||||
uploadAvatar(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadAvatar(_ image: UIImage) {
|
||||
isUploadingAvatar = true
|
||||
avatarUploadError = nil
|
||||
|
||||
Task {
|
||||
if let url = await COSManagerAdapter.shared.uploadUIImage(image, apiService: apiService) {
|
||||
await MainActor.run {
|
||||
self.isUploadingAvatar = false
|
||||
self.updateUserAvatar(url)
|
||||
}
|
||||
} else {
|
||||
await MainActor.run {
|
||||
self.isUploadingAvatar = false
|
||||
self.avatarUploadError = "头像上传失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateUserAvatar(_ avatarUrl: String) {
|
||||
guard let userInfo = userInfo else { return }
|
||||
|
||||
isUpdatingUser = true
|
||||
updateUserError = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
let ticket = await UserInfoManager.getCurrentUserTicket() ?? ""
|
||||
let request = UpdateUserRequest(avatar: avatarUrl, nick: nil, uid: userInfo.uid ?? 0, ticket: ticket)
|
||||
let response: UpdateUserResponse = try await apiService.request(request)
|
||||
|
||||
await MainActor.run {
|
||||
self.isUpdatingUser = false
|
||||
if response.code == 200 {
|
||||
// 刷新用户信息
|
||||
self.loadUserInfo()
|
||||
} else {
|
||||
self.updateUserError = response.message
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.isUpdatingUser = false
|
||||
self.updateUserError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Nickname Management
|
||||
func onNicknameTapped() {
|
||||
nicknameInput = userInfo?.nick ?? ""
|
||||
isEditingNickname = true
|
||||
}
|
||||
|
||||
func onNicknameInputChanged(_ text: String) {
|
||||
nicknameInput = String(text.prefix(15))
|
||||
}
|
||||
|
||||
func onNicknameEditConfirmed() {
|
||||
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
|
||||
isEditingNickname = false
|
||||
updateUserNickname(trimmed)
|
||||
}
|
||||
|
||||
private func updateUserNickname(_ nickname: String) {
|
||||
guard let userInfo = userInfo else { return }
|
||||
|
||||
isUpdatingUser = true
|
||||
updateUserError = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
let ticket = await UserInfoManager.getCurrentUserTicket() ?? ""
|
||||
let request = UpdateUserRequest(avatar: nil, nick: nickname, uid: userInfo.uid ?? 0, ticket: ticket)
|
||||
let response: UpdateUserResponse = try await apiService.request(request)
|
||||
|
||||
await MainActor.run {
|
||||
self.isUpdatingUser = false
|
||||
if response.code == 200 {
|
||||
// 刷新用户信息
|
||||
self.loadUserInfo()
|
||||
} else {
|
||||
self.updateUserError = response.message
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.isUpdatingUser = false
|
||||
self.updateUserError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Settings Actions
|
||||
func onPersonalInfoPermissionsTapped() {
|
||||
showPrivacyPolicy = true
|
||||
}
|
||||
|
||||
func onHelpTapped() {
|
||||
showUserAgreement = true
|
||||
}
|
||||
|
||||
func onClearCacheTapped() {
|
||||
// TODO: 实现清除缓存逻辑
|
||||
debugInfoSync("🗑️ 清除缓存")
|
||||
}
|
||||
|
||||
func onCheckUpdatesTapped() {
|
||||
// TODO: 实现检查更新逻辑
|
||||
debugInfoSync("🔄 检查更新")
|
||||
}
|
||||
|
||||
func onDeactivateAccountTapped() {
|
||||
showDeactivateAccount = true
|
||||
}
|
||||
|
||||
func onAboutUsTapped() {
|
||||
showAboutUs = true
|
||||
}
|
||||
|
||||
func onLogoutTapped() {
|
||||
showLogoutConfirmation = true
|
||||
}
|
||||
|
||||
func onLogoutConfirmed() {
|
||||
Task {
|
||||
await UserInfoManager.clearAllAuthenticationData()
|
||||
await MainActor.run {
|
||||
onLogout?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WebView Dismissal
|
||||
func onPrivacyPolicyDismissed() {
|
||||
showPrivacyPolicy = false
|
||||
}
|
||||
|
||||
func onUserAgreementDismissed() {
|
||||
showUserAgreement = false
|
||||
}
|
||||
|
||||
func onDeactivateAccountDismissed() {
|
||||
showDeactivateAccount = false
|
||||
}
|
||||
}
|
@@ -144,7 +144,8 @@
|
||||
"appSetting.logoutConfirmation.confirm" = "Confirm Logout";
|
||||
"appSetting.logoutConfirmation.message" = "Are you sure you want to logout from your current account?";
|
||||
"appSetting.deactivateAccount" = "Deactivate Account";
|
||||
"appSetting.logoutAccount" = "Log out of account";
|
||||
"appSetting.logoutAccount" = "Log out of account";
|
||||
"app_settings.not_set" = "Not set";
|
||||
|
||||
// MARK: - Detail
|
||||
"detail.title" = "Enjoy your life";
|
||||
|
@@ -140,7 +140,8 @@
|
||||
"appSetting.logoutConfirmation.confirm" = "确认退出";
|
||||
"appSetting.logoutConfirmation.message" = "确定要退出当前账户吗?";
|
||||
"appSetting.deactivateAccount" = "注销帐号";
|
||||
"appSetting.logoutAccount" = "退出账户";
|
||||
"appSetting.logoutAccount" = "退出账户";
|
||||
"app_settings.not_set" = "未设置";
|
||||
|
||||
// MARK: - Detail
|
||||
"detail.title" = "享受你的生活";
|
||||
|
@@ -373,42 +373,42 @@ struct AppSettingView: View {
|
||||
}
|
||||
|
||||
// MARK: - 设置行组件
|
||||
struct SettingRow: View {
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let action: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
action?()
|
||||
}) {
|
||||
HStack(spacing: 16) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
if !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if action != nil {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.disabled(action == nil)
|
||||
}
|
||||
}
|
||||
//struct SettingRow: View {
|
||||
// let title: String
|
||||
// let subtitle: String
|
||||
// let action: (() -> Void)?
|
||||
//
|
||||
// var body: some View {
|
||||
// Button(action: {
|
||||
// action?()
|
||||
// }) {
|
||||
// HStack(spacing: 16) {
|
||||
// HStack {
|
||||
// Text(title)
|
||||
// .font(.system(size: 16))
|
||||
// .foregroundColor(.white)
|
||||
// .multilineTextAlignment(.leading)
|
||||
//
|
||||
// Spacer()
|
||||
//
|
||||
// if !subtitle.isEmpty {
|
||||
// Text(subtitle)
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.7))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
//
|
||||
// if action != nil {
|
||||
// Image(systemName: "chevron.right")
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.5))
|
||||
// }
|
||||
// }
|
||||
// .padding(.horizontal, 16)
|
||||
// .padding(.vertical, 12)
|
||||
// }
|
||||
// .disabled(action == nil)
|
||||
// }
|
||||
//}
|
||||
|
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import UIKit
|
||||
import PhotosUI
|
||||
|
||||
public struct CameraPicker: UIViewControllerRepresentable {
|
||||
public struct _CameraPicker: UIViewControllerRepresentable {
|
||||
public var onImagePicked: (UIImage?) -> Void
|
||||
public init(onImagePicked: @escaping (UIImage?) -> Void) {
|
||||
self.onImagePicked = onImagePicked
|
||||
|
@@ -42,14 +42,14 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
Button(LocalizedString("web_view.open_webpage", comment: "")) {
|
||||
// 预览时不执行任何操作
|
||||
}
|
||||
}
|
||||
.webView(
|
||||
isPresented: .constant(true),
|
||||
url: URL(string: "https://www.apple.com")
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// VStack {
|
||||
// Button(LocalizedString("web_view.open_webpage", comment: "")) {
|
||||
// // 预览时不执行任何操作
|
||||
// }
|
||||
// }
|
||||
// .webView(
|
||||
// isPresented: .constant(true),
|
||||
// url: URL(string: "https://www.apple.com")
|
||||
// )
|
||||
//}
|
||||
|
Reference in New Issue
Block a user