
- 修改COSManagerAdapter以支持新的TCCos组件,确保与腾讯云COS的兼容性。 - 在CreateFeedFeature中新增图片上传相关状态和Action,优化图片选择与上传逻辑。 - 更新CreateFeedView以整合图片上传功能,提升用户体验。 - 在多个视图中添加键盘状态管理,改善用户交互体验。 - 新增COS相关的测试文件,确保功能的正确性和稳定性。
359 lines
10 KiB
Swift
359 lines
10 KiB
Swift
import SwiftUI
|
|
import ComposableArchitecture
|
|
|
|
// MARK: - COS 主界面组件
|
|
|
|
/// COS 主界面组件
|
|
/// 整合 Token 状态、上传进度、错误处理等功能
|
|
public struct COSView: View {
|
|
|
|
// MARK: - Properties
|
|
|
|
let store: StoreOf<COSFeature>
|
|
|
|
// MARK: - Initialization
|
|
|
|
public init(store: StoreOf<COSFeature>) {
|
|
self.store = store
|
|
}
|
|
|
|
// MARK: - Body
|
|
|
|
public var body: some View {
|
|
WithViewStore(store, observe: { $0 }) { viewStore in
|
|
VStack(spacing: 16) {
|
|
// Token 状态显示
|
|
if let tokenState = viewStore.tokenState {
|
|
TokenStatusView(tokenState: tokenState)
|
|
}
|
|
|
|
// 配置状态显示
|
|
if let configState = viewStore.configurationState {
|
|
ConfigurationStatusView(configState: configState)
|
|
}
|
|
|
|
// 上传状态显示
|
|
if let uploadState = viewStore.uploadState {
|
|
UploadProgressView(uploadState: uploadState)
|
|
}
|
|
|
|
// 操作按钮区域
|
|
COSActionButtonsView(store: store)
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.onAppear {
|
|
viewStore.send(.onAppear)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Token 状态视图
|
|
|
|
/// Token 状态显示组件
|
|
private struct TokenStatusView: View {
|
|
let tokenState: TokenState
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Image(systemName: tokenIcon)
|
|
.foregroundColor(tokenColor)
|
|
|
|
Text("Token 状态")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
if tokenState.isLoading {
|
|
ProgressView()
|
|
.scaleEffect(0.8)
|
|
}
|
|
}
|
|
|
|
if let token = tokenState.currentToken {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("存储桶: \(token.bucket)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("地域: \(token.region)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("过期时间: \(formatRelativeTime(token.expirationDate))")
|
|
.font(.caption)
|
|
.foregroundColor(token.isExpired ? .red : .green)
|
|
}
|
|
} else {
|
|
Text("未获取 Token")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if let error = tokenState.error {
|
|
Text("错误: \(error)")
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemGray6))
|
|
.cornerRadius(8)
|
|
}
|
|
|
|
private var tokenIcon: String {
|
|
if tokenState.isLoading {
|
|
return "arrow.clockwise"
|
|
} else if let token = tokenState.currentToken {
|
|
return token.isExpired ? "exclamationmark.triangle" : "checkmark.circle"
|
|
} else {
|
|
return "questionmark.circle"
|
|
}
|
|
}
|
|
|
|
private var tokenColor: Color {
|
|
if tokenState.isLoading {
|
|
return .blue
|
|
} else if let token = tokenState.currentToken {
|
|
return token.isExpired ? .red : .green
|
|
} else {
|
|
return .orange
|
|
}
|
|
}
|
|
|
|
private func formatRelativeTime(_ date: Date) -> String {
|
|
let formatter = RelativeDateTimeFormatter()
|
|
formatter.unitsStyle = .full
|
|
return formatter.localizedString(for: date, relativeTo: Date())
|
|
}
|
|
}
|
|
|
|
// MARK: - 配置状态视图
|
|
|
|
/// 配置状态显示组件
|
|
private struct ConfigurationStatusView: View {
|
|
let configState: ConfigurationState
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Image(systemName: configIcon)
|
|
.foregroundColor(configColor)
|
|
|
|
Text("服务状态")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
}
|
|
|
|
Text(statusMessage)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
if let error = configState.error {
|
|
Text("错误: \(error)")
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemGray6))
|
|
.cornerRadius(8)
|
|
}
|
|
|
|
private var configIcon: String {
|
|
switch configState.serviceStatus {
|
|
case .notInitialized:
|
|
return "xmark.circle"
|
|
case .initializing:
|
|
return "arrow.clockwise"
|
|
case .initialized:
|
|
return "checkmark.circle"
|
|
case .failed:
|
|
return "exclamationmark.triangle"
|
|
}
|
|
}
|
|
|
|
private var configColor: Color {
|
|
switch configState.serviceStatus {
|
|
case .notInitialized:
|
|
return .orange
|
|
case .initializing:
|
|
return .blue
|
|
case .initialized:
|
|
return .green
|
|
case .failed:
|
|
return .red
|
|
}
|
|
}
|
|
|
|
private var statusMessage: String {
|
|
switch configState.serviceStatus {
|
|
case .notInitialized:
|
|
return "服务未初始化"
|
|
case .initializing:
|
|
return "正在初始化服务..."
|
|
case .initialized(let config):
|
|
return "服务已初始化 - \(config.bucket)"
|
|
case .failed(let error):
|
|
return "初始化失败: \(error)"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 上传进度视图
|
|
|
|
/// 上传进度显示组件
|
|
private struct UploadProgressView: View {
|
|
let uploadState: UploadState
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Image(systemName: uploadIcon)
|
|
.foregroundColor(uploadColor)
|
|
|
|
Text("上传状态")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
if uploadState.isUploading {
|
|
Button("取消") {
|
|
// TODO: 实现取消上传
|
|
}
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
|
|
if let task = uploadState.currentTask {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("文件: \(task.fileName)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("大小: \(ByteCountFormatter.string(fromByteCount: Int64(task.imageData.count), countStyle: .file))")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
if uploadState.isUploading {
|
|
ProgressView(value: uploadState.progress)
|
|
.progressViewStyle(LinearProgressViewStyle())
|
|
|
|
Text("\(Int(uploadState.progress * 100))%")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if let result = uploadState.result {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("上传成功")
|
|
.font(.caption)
|
|
.foregroundColor(.green)
|
|
|
|
Text(result)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(2)
|
|
}
|
|
}
|
|
|
|
if let error = uploadState.error {
|
|
Text("上传失败: \(error)")
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemGray6))
|
|
.cornerRadius(8)
|
|
}
|
|
|
|
private var uploadIcon: String {
|
|
if uploadState.isUploading {
|
|
return "arrow.up.circle"
|
|
} else if uploadState.result != nil {
|
|
return "checkmark.circle"
|
|
} else if uploadState.error != nil {
|
|
return "xmark.circle"
|
|
} else {
|
|
return "arrow.up.circle"
|
|
}
|
|
}
|
|
|
|
private var uploadColor: Color {
|
|
if uploadState.isUploading {
|
|
return .blue
|
|
} else if uploadState.result != nil {
|
|
return .green
|
|
} else if uploadState.error != nil {
|
|
return .red
|
|
} else {
|
|
return .gray
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 操作按钮视图
|
|
|
|
/// 操作按钮组件
|
|
private struct COSActionButtonsView: View {
|
|
let store: StoreOf<COSFeature>
|
|
|
|
var body: some View {
|
|
WithViewStore(store, observe: { $0 }) { viewStore in
|
|
VStack(spacing: 12) {
|
|
// 主要操作按钮
|
|
HStack(spacing: 12) {
|
|
Button("获取 Token") {
|
|
viewStore.send(.token(.getToken))
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(viewStore.tokenState?.isLoading == true)
|
|
|
|
Button("刷新 Token") {
|
|
viewStore.send(.token(.refreshToken))
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.disabled(viewStore.tokenState?.isLoading == true)
|
|
}
|
|
|
|
HStack(spacing: 12) {
|
|
Button("重试") {
|
|
viewStore.send(.retry)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
|
|
Button("重置") {
|
|
viewStore.send(.resetAll)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.foregroundColor(.red)
|
|
}
|
|
|
|
Button("健康检查") {
|
|
viewStore.send(.checkHealth)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 预览
|
|
|
|
#Preview {
|
|
COSView(
|
|
store: Store(
|
|
initialState: COSFeature.State(),
|
|
reducer: { COSFeature() }
|
|
)
|
|
)
|
|
} |