feat: 样式优化

This commit is contained in:
jinyaqiu 2025-09-03 11:42:14 +08:00
parent b118b91001
commit df32ea71bb
11 changed files with 565 additions and 34 deletions

View File

@ -1,5 +1,5 @@
{
"originHash" : "e8f130fe30ac6cdc940ef06ee1e8535e9f46ffee6aeead1722b9525562f6ce08",
"originHash" : "7ea295cc5e3eb8ef644b89ce2b47a7600994b67c8582ee354b643cd63250740d",
"pins" : [
{
"identity" : "alamofire",
@ -9,6 +9,51 @@
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2"
}
},
{
"identity" : "cocoalumberjack",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
"state" : {
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
"version" : "3.9.0"
}
},
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/airbnb/lottie-spm.git",
"state" : {
"revision" : "04f2fd18cc9404a0a0917265a449002674f24ec9",
"version" : "4.5.2"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit.git",
"state" : {
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
"version" : "3.0.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log",
"state" : {
"revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
"version" : "1.6.4"
}
},
{
"identity" : "waterfallgrid",
"kind" : "remoteSourceControl",
"location" : "https://github.com/paololeonardi/WaterfallGrid.git",
"state" : {
"revision" : "c7c08652c3540adf8e48409c351879b4caea7e89",
"version" : "1.1.0"
}
}
],
"version" : 3

View File

@ -16,6 +16,7 @@ enum AppRoute: Hashable {
case about
case permissionManagement
case feedback
case blindList
@ViewBuilder
var view: some View {
@ -48,6 +49,8 @@ enum AppRoute: Hashable {
PermissionManagementView()
case .feedback:
FeedbackView()
case .blindList:
BlindListView()
}
}
}

View File

@ -196,27 +196,27 @@ struct UserProfileModal: View {
}
.buttonStyle(PlainButtonStyle())
// Box
// Button(action: {
// Router.shared.navigate(to: .mediaUpload)
// }) {
// HStack(spacing: 16) {
// SVGImage(svgName: "Box")
// .foregroundColor(.orange)
// .frame(width: 20, height: 20)
//Box
Button(action: {
Router.shared.navigate(to: .blindList)
}) {
HStack(spacing: 16) {
SVGImage(svgName: "Box")
.foregroundColor(.orange)
.frame(width: 20, height: 20)
// Text("My Blind Box")
// .font(Typography.font(for: .body))
// .fontWeight(.bold)
// .foregroundColor(.themeTextMessageMain)
Text("My Blind Box")
.font(Typography.font(for: .body))
.fontWeight(.bold)
.foregroundColor(.themeTextMessageMain)
// Spacer()
// }
// .padding()
// .cornerRadius(10)
// .contentShape(Rectangle())
// }
// .buttonStyle(PlainButtonStyle())
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
// setting
Button(action: {

View File

@ -25,7 +25,7 @@ struct AboutUsView: View {
VStack(spacing: 0) {
// IP Address Section
VStack(spacing: 12) {
SVGImage(svgName: "AboutIP")
SVGImageHtml(svgName: "AboutIP")
.frame(width: 102, height: 102)
}
.frame(maxWidth: .infinity)
@ -33,11 +33,11 @@ struct AboutUsView: View {
// Version & ICP Info
VStack(spacing: 12) {
Text("Version : 1.1.1")
Text("Version : 2.0.0")
.font(.system(size: 12))
.foregroundColor(.themeTextMessageMain)
Text("ICP 备案号: 京ICP备XXXXXXXX号")
Text("ICP 备案号: 沪ICP备2025133004号-2A")
.font(.system(size: 12))
.foregroundColor(.themeTextMessageMain)
}

View File

@ -121,7 +121,7 @@ struct AccountView: View {
}) {
Text("Confirm")
.foregroundColor(.themeTextWhite)
.foregroundColor(.themeTextMessage)
.font(.system(size: 12))
.frame(maxWidth: .infinity)
.padding()

View File

@ -0,0 +1,483 @@
import SwiftUI
import AVKit
import WaterfallGrid
// MARK: - API Response Models for BlindList
struct BlindListMaterialResponse: Decodable {
let code: Int
let data: [BlindBoxItem]
}
struct BlindBoxItem: Identifiable, Decodable {
let id: Int64
let boxCode: String
let userId: Int64
let name: String
let boxType: String
let features: String?
let resultFile: FileInfo?
let status: String
let workflowInstanceId: Int64?
let videoGenerateTime: String?
let createTime: String
let coverFile: FileInfo?
let description: String
struct FileInfo: Decodable {
let id: String
let fileName: String
let url: String
let metadata: [String: String]?
enum CodingKeys: String, CodingKey {
case id
case fileName = "file_name"
case url
case metadata
}
}
enum CodingKeys: String, CodingKey {
case id
case boxCode = "box_code"
case userId = "user_id"
case name
case boxType = "box_type"
case features
case resultFile = "result_file"
case status
case workflowInstanceId = "workflow_instance_id"
case videoGenerateTime = "video_generate_time"
case createTime = "create_time"
case coverFile = "cover_file"
case description
}
}
enum BlindListMemoryMediaType: Equatable {
case image(String)
case video(url: String, previewUrl: String)
var isVideo: Bool {
if case .video = self { return true }
return false
}
var url: String {
switch self {
case .image(let url):
return url
case .video(let url, _):
return url
}
}
}
// MARK: - View Models
struct BlindListFileInfo {
let id: String
let fileName: String
let url: String
}
struct BlindListMemoryItem: Identifiable {
let id: String
let name: String
let description: String
let fileInfo: BlindListFileInfo
let previewFileInfo: BlindListFileInfo
var title: String { name }
var subtitle: String { description }
var mediaType: BlindListMemoryMediaType {
// Determine media type based on file extension or other criteria
// For now, default to image
return .image(fileInfo.url)
}
var aspectRatio: CGFloat { 1.0 }
}
struct BlindListView: View {
@Environment(\.dismiss) private var dismiss
@State private var memories: [BlindListMemoryItem] = []
@State private var isLoading = false
@State private var errorMessage: String?
@State private var selectedMemory: BlindListMemoryItem? = nil
let columns = [
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1)
]
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
//
HStack {
Button(action: {
self.dismiss()
}) {
Image(systemName: "chevron.left")
.foregroundColor(.themeTextMessageMain)
.font(.system(size: 20))
}
Spacer()
Text("我的盲盒")
.foregroundColor(.themeTextMessageMain)
.font(Typography.font(for: .body, family: .quicksandBold))
Spacer()
}
.padding()
.background(Color.themeTextWhiteSecondary)
//
ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea()
ScrollView {
WaterfallGrid(memories) { memory in
BlindListMemoryCard(memory: memory)
.onTapGesture {
withAnimation(.spring()) {
selectedMemory = memory
}
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
}
}
//
if let memory = selectedMemory {
BlindListFullScreenMediaView(memory: memory, isPresented: $selectedMemory)
.transition(.opacity)
.zIndex(1)
}
}
}
.navigationBarBackButtonHidden(true)
.onAppear {
fetchList()
}
}
private func fetchList() {
isLoading = true
errorMessage = nil
NetworkService.shared.get(path: "/blind_boxs/query", parameters: nil) { [self] (result: Result<BlindListMaterialResponse, NetworkError>) in
DispatchQueue.main.async { [self] in
self.isLoading = false
switch result {
case .success(let response):
print("✅ Successfully fetched \(response.data.count) blind box items")
// Convert BlindBoxItem to BlindListMemoryItem
self.memories = response.data
.filter { $0.status == "Opened" }
.map { item in
BlindListMemoryItem(
id: String(item.id),
name: item.name,
description: item.description,
fileInfo: BlindListFileInfo(
id: item.resultFile?.id ?? "",
fileName: item.resultFile?.fileName ?? "",
url: item.resultFile?.url ?? ""
),
previewFileInfo: BlindListFileInfo(
id: item.coverFile?.id ?? "",
fileName: item.coverFile?.fileName ?? "",
url: item.coverFile?.url ?? ""
)
)
}
case .failure(let error):
self.errorMessage = error.localizedDescription
print("❌ Failed to fetch blind box items: \(error.localizedDescription)")
}
}
}
}
}
struct BlindListFullScreenMediaView: View {
let memory: BlindListMemoryItem
@Binding var isPresented: BlindListMemoryItem?
@State private var isVideoPlaying = false
@State private var showControls = true
@State private var controlsTimer: Timer? = nil
@State private var imageAspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
imageAspectRatio = 16/9
isLoading = false
return
}
imageAspectRatio = width / height
isLoading = false
}
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
ZStack {
GeometryReader { geometry in
switch memory.mediaType {
case .image(let url):
if let imageURL = URL(string: url) {
AsyncImage(url: imageURL) { phase in
switch phase {
case .success(let image):
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear {
if let uiImage = image.asUIImage() {
let size = uiImage.size
imageAspectRatio = size.width / size.height
isLoading = false
}
}
case .failure(_):
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.red)
case .empty:
ProgressView()
@unknown default:
EmptyView()
}
}
}
case .video(_, let previewUrl):
GeometryReader { geometry in
ZStack {
Color.clear
BlindListVideoPlayer(url: memory.mediaType.url, isPlaying: $isVideoPlaying)
.aspectRatio(imageAspectRatio, contentMode: .fit)
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
.onAppear {
if let previewUrl = URL(string: previewUrl) {
loadAspectRatio(from: previewUrl)
}
isVideoPlaying = true
}
.onDisappear {
isVideoPlaying = false
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
}
VStack {
HStack {
Button(action: {
withAnimation(.spring()) {
isPresented = nil
}
}) {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.padding(12)
.background(Circle().fill(Color.black.opacity(0.4)))
}
.padding(.leading, 16)
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
Spacer()
}
Spacer()
}
.zIndex(2)
}
.ignoresSafeArea()
.statusBar(hidden: true)
}
.onTapGesture {
if case .video = memory.mediaType {
withAnimation(.easeInOut) {
showControls.toggle()
}
}
}
.statusBar(hidden: true)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
controlsTimer?.invalidate()
}
}
}
struct BlindListVideoPlayer: UIViewControllerRepresentable {
let url: String
@Binding var isPlaying: Bool
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let player = AVPlayer(url: URL(string: url)!)
controller.player = player
controller.showsPlaybackControls = true
controller.videoGravity = .resizeAspect
controller.view.backgroundColor = .clear
controller.view.isOpaque = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if isPlaying {
uiViewController.player?.play()
} else {
uiViewController.player?.pause()
}
}
}
struct BlindListMemoryCard: View {
let memory: BlindListMemoryItem
@State private var aspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
aspectRatio = 16/9
isLoading = false
return
}
aspectRatio = width / height
isLoading = false
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ZStack {
Group {
switch memory.mediaType {
case .image(let url):
if let url = URL(string: url) {
AsyncImage(url: url) { phase in
Group {
if let image = phase.image {
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * aspectRatio),
height: min(geometry.size.height, geometry.size.width / aspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.aspectRatio(aspectRatio, contentMode: aspectRatio > 1 ? .fit : .fill)
.onAppear {
if let uiImage = image.asUIImage() {
let size = uiImage.size
aspectRatio = size.width / size.height
isLoading = false
}
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
}
}
}
}
case .video(_, let previewUrl):
if let previewUrl = URL(string: previewUrl) {
AsyncImage(url: previewUrl) { phase in
Group {
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear {
loadAspectRatio(from: previewUrl)
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
}
}
}
} else {
Color.gray.opacity(0.3)
}
}
}
.frame(
width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) / (isLoading ? 1 : aspectRatio)
)
.clipped()
.cornerRadius(12)
if case .video = memory.mediaType {
Image(systemName: "play.circle.fill")
.font(.system(size: 40))
.foregroundColor(.white.opacity(0.9))
.shadow(radius: 3)
}
}
VStack(alignment: .leading, spacing: 4) {
Text(memory.title)
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
.lineLimit(1)
Text(memory.subtitle)
.font(.system(size: 14))
.foregroundColor(.themeTextMessageMain)
.lineLimit(2)
}
.padding(.horizontal, 2)
.padding(.bottom, 8)
}
}
}

View File

@ -28,16 +28,18 @@ struct PermissionManagementView: View {
title: "Gallery Permissions",
isEnabled: photoLibraryStatus == .authorized
) {
requestPhotoLibraryPermission() //
requestPhotoLibraryPermission()
}
.background(Color.white)
// 2.
PermissionRow(
title: "Notification Permissions",
isEnabled: notificationStatus == .authorized
) {
requestNotificationPermission() //
requestNotificationPermission()
}
.background(Color.white)
}
.background(Color.white)
.cornerRadius(16)
@ -159,13 +161,6 @@ struct PermissionRow: View {
.padding()
}
.buttonStyle(PlainButtonStyle())
//
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color(.systemGray6)),
alignment: .bottom
)
}
}

View File

@ -60,7 +60,11 @@ struct SettingsView: View {
settingRow(
icon: "Suport",
title: "Support & Service",
action: {}
action: {
if let url = URL(string: "https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd") {
UIApplication.shared.open(url)
}
}
)
//

View File

@ -315,6 +315,7 @@ struct MediaUploadView: View {
///
private func handleContinue() {
//
Router.shared.navigate(to: .blindBox(mediaType: .video))
let uploadResults = uploadManager.uploadResults
guard !uploadResults.isEmpty else {
print("⚠️ 没有可用的文件ID")