feat: 头像选择上传
This commit is contained in:
parent
5df804d115
commit
44de40cf83
BIN
wake/Assets/.DS_Store
vendored
Normal file
BIN
wake/Assets/.DS_Store
vendored
Normal file
Binary file not shown.
@ -20,6 +20,7 @@ enum TypographyStyle {
|
|||||||
case largeTitle // 大标题
|
case largeTitle // 大标题
|
||||||
case headline // 大标题
|
case headline // 大标题
|
||||||
case title // 标题
|
case title // 标题
|
||||||
|
case title2 // 标题
|
||||||
case body // 正文
|
case body // 正文
|
||||||
case subtitle // 副标题
|
case subtitle // 副标题
|
||||||
case caption // 说明文字
|
case caption // 说明文字
|
||||||
@ -39,13 +40,14 @@ struct Typography {
|
|||||||
// MARK: - 配置
|
// MARK: - 配置
|
||||||
|
|
||||||
/// 默认字体库
|
/// 默认字体库
|
||||||
private static let defaultFontFamily: FontFamily = .quicksand
|
private static let defaultFontFamily: FontFamily = .quicksandRegular
|
||||||
|
|
||||||
/// 文本样式配置表
|
/// 文本样式配置表
|
||||||
private static let styleConfig: [TypographyStyle: TypographyConfig] = [
|
private static let styleConfig: [TypographyStyle: TypographyConfig] = [
|
||||||
.largeTitle: TypographyConfig(size: 32, weight: .heavy, textStyle: .largeTitle),
|
.largeTitle: TypographyConfig(size: 32, weight: .heavy, textStyle: .largeTitle),
|
||||||
.headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline),
|
.headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline),
|
||||||
.title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2),
|
.title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2),
|
||||||
|
.title2: TypographyConfig(size: 18, weight: .bold, textStyle: .title2),
|
||||||
.body: TypographyConfig(size: 16, weight: .regular, textStyle: .body),
|
.body: TypographyConfig(size: 16, weight: .regular, textStyle: .body),
|
||||||
.subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline),
|
.subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline),
|
||||||
.caption: TypographyConfig(size: 12, weight: .regular, textStyle: .caption1),
|
.caption: TypographyConfig(size: 12, weight: .regular, textStyle: .caption1),
|
||||||
|
|||||||
84
wake/View/Owner/UserInfo/AvatarPicker.swift
Normal file
84
wake/View/Owner/UserInfo/AvatarPicker.swift
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct AvatarPicker: View {
|
||||||
|
@StateObject private var uploadManager = MediaUploadManager()
|
||||||
|
@State private var showMediaPicker = false
|
||||||
|
@State private var isUploading = false
|
||||||
|
@Binding var selectedImage: UIImage?
|
||||||
|
|
||||||
|
public init(selectedImage: Binding<UIImage?>) {
|
||||||
|
self._selectedImage = selectedImage
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
// Avatar Image
|
||||||
|
Button(action: {
|
||||||
|
showMediaPicker = true
|
||||||
|
}) {
|
||||||
|
ZStack {
|
||||||
|
if let selectedImage = selectedImage {
|
||||||
|
Image(uiImage: selectedImage)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 225, height: 225)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||||
|
} else {
|
||||||
|
// Default SVG avatar
|
||||||
|
SVGImage(svgName: "Avatar")
|
||||||
|
.frame(width: 225, height: 225)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUploading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle())
|
||||||
|
.scaleEffect(1.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload button
|
||||||
|
Button(action: {
|
||||||
|
showMediaPicker = true
|
||||||
|
}) {
|
||||||
|
Text("Upload from Gallery")
|
||||||
|
.font(Typography.font(for: .subtitle, family: .inter))
|
||||||
|
.fontWeight(.regular)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.foregroundColor(.black)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.fill(Color.themePrimaryLight)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.frame(width: .infinity)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showMediaPicker) {
|
||||||
|
MediaPicker(
|
||||||
|
selectedMedia: $uploadManager.selectedMedia,
|
||||||
|
imageSelectionLimit: 1,
|
||||||
|
videoSelectionLimit: 0,
|
||||||
|
allowedMediaTypes: .imagesOnly,
|
||||||
|
selectionMode: .single,
|
||||||
|
onDismiss: {
|
||||||
|
showMediaPicker = false
|
||||||
|
if !uploadManager.selectedMedia.isEmpty {
|
||||||
|
isUploading = true
|
||||||
|
uploadManager.startUpload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onChange(of: uploadManager.uploadStatus) { _ in
|
||||||
|
if let firstMedia = uploadManager.selectedMedia.first,
|
||||||
|
case .image(let image) = firstMedia,
|
||||||
|
uploadManager.isAllUploaded {
|
||||||
|
selectedImage = image
|
||||||
|
isUploading = false
|
||||||
|
uploadManager.clearAllMedia()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,9 +13,20 @@ struct UserInfo: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
ReturnButton {
|
||||||
|
print("返回")
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Text("Complete Your Profile")
|
||||||
|
.font(Typography.font(for: .title2, family: .quicksandBold))
|
||||||
|
.foregroundColor(.themeTextMessageMain)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
|
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
|
||||||
.font(Typography.font(for: .small))
|
.font(Typography.font(for: .caption))
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
@ -31,59 +42,32 @@ struct UserInfo: View {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
.padding(10)
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// Title
|
// Title
|
||||||
Text("Add Your Avatar")
|
Text("Add Your Avatar")
|
||||||
.font(Typography.font(for: .title))
|
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
ZStack {
|
ZStack {
|
||||||
// Show either the SVG or the uploaded image
|
AvatarPicker(selectedImage: $avatarImage)
|
||||||
if let avatarImage = avatarImage {
|
|
||||||
Image(uiImage: avatarImage)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(width: 200, height: 200)
|
|
||||||
.clipShape(Circle())
|
|
||||||
} else {
|
|
||||||
SVGImage(svgName: "Avatar")
|
|
||||||
.frame(width: 200, height: 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the AvatarUploader is on top and covers the entire area
|
|
||||||
AvatarUploader(selectedImage: $avatarImage, size: 200)
|
|
||||||
.contentShape(Rectangle()) // This makes the entire area tappable
|
|
||||||
}
|
|
||||||
.frame(width: 200, height: 200)
|
|
||||||
.padding(.vertical, 20)
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
Button(action: {
|
|
||||||
// Action for first button
|
|
||||||
}) {
|
|
||||||
Text("Upload from Gallery")
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 25)
|
|
||||||
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.padding(.top, 20)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// Action for second button
|
// Action for second button
|
||||||
}) {
|
}) {
|
||||||
Text("Take a Photo")
|
Text("Take a Photo")
|
||||||
|
.font(Typography.font(for: .subtitle, family: .inter))
|
||||||
|
.fontWeight(.regular)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 25)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(Color(red: 1.0, green: 0.973, blue: 0.871))
|
.fill(Color.themePrimaryLight)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,30 +78,22 @@ struct UserInfo: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
// Action for next button
|
// Action for next button
|
||||||
}) {
|
}) {
|
||||||
Text("Next")
|
Text("Continue")
|
||||||
|
.font(Typography.font(for: .body))
|
||||||
|
.fontWeight(.bold)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding()
|
.padding(16)
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 25)
|
RoundedRectangle(cornerRadius: 25)
|
||||||
.fill(Color(red: 1.0, green: 0.714, blue: 0.271))
|
.fill(Color.themePrimary)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.navigationTitle("Complete Your Profile")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA
|
.background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button(action: {
|
|
||||||
dismiss()
|
|
||||||
}) {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,20 @@ struct SVGImage: UIViewRepresentable {
|
|||||||
webView.scrollView.isScrollEnabled = false
|
webView.scrollView.isScrollEnabled = false
|
||||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
|
||||||
if let url = Bundle.main.url(forResource: svgName, withExtension: "svg") {
|
// 1. 获取 SVG 文件路径(注意:移除了 inDirectory 参数)
|
||||||
|
guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
|
||||||
|
print("❌ 无法找到 SVG 文件: \(svgName).svg")
|
||||||
|
// 打印所有可用的资源文件,用于调试
|
||||||
|
if let resourcePath = Bundle.main.resourcePath {
|
||||||
|
print("可用的资源文件: \(try? FileManager.default.contentsOfDirectory(atPath: resourcePath))")
|
||||||
|
}
|
||||||
|
return webView
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建文件 URL
|
||||||
|
let fileURL = URL(fileURLWithPath: path)
|
||||||
|
|
||||||
|
// 3. 创建 HTML 字符串
|
||||||
let htmlString = """
|
let htmlString = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@ -24,47 +37,32 @@ struct SVGImage: UIViewRepresentable {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
|
<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
|
||||||
<img src="\(url.absoluteString)" style="max-width:100%; max-height:100%;" />
|
<img src="\(fileURL.lastPathComponent)" />
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
webView.loadHTMLString(htmlString, baseURL: nil)
|
// 4. 加载 HTML 字符串
|
||||||
}
|
webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
|
||||||
|
|
||||||
return webView
|
return webView
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - View Modifier for SVG Image
|
|
||||||
struct SVGImageModifier: ViewModifier {
|
|
||||||
let size: CGSize
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.frame(width: size.width, height: size.height)
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
func svgImageStyle(size: CGSize) -> some View {
|
|
||||||
self.modifier(SVGImageModifier(size: size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
// SVGImage(svgName: "Avatar")
|
|
||||||
|
|||||||
@ -48,7 +48,9 @@ struct WakeApp: App {
|
|||||||
.environmentObject(authState)
|
.environmentObject(authState)
|
||||||
} else {
|
} else {
|
||||||
// 未登录:显示登录界面
|
// 未登录:显示登录界面
|
||||||
ContentView()
|
// ContentView()
|
||||||
|
// .environmentObject(authState)
|
||||||
|
UserInfo()
|
||||||
.environmentObject(authState)
|
.environmentObject(authState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user