Files
mao1/Rumeng/_archived/FridgeCode.swift
maomao 2543c5f782 Initial: 接手 Rumeng app,清掉前夫哥痕迹
- 改 Bundle ID rumeng-v1.0.Rumeng → syke.maomao.app
- 显示名 如梦 → Syke
- 头像换白图占位
- 删除沈晏头像图片、空目录、bridge 旧数据
- ServerConfig.swift 含明文 token,已加入 .gitignore
2026-06-05 09:32:13 +08:00

237 lines
11 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// MARK: - ViewModel
@MainActor
final class FridgeViewModel: ObservableObject {
@Published var notes: [FridgeNote] = []
private let session = URLSession(configuration: .default)
struct FridgeNote: Identifiable {
let id: String; let text: String; let role: String; let createdAt: String
var isMine: Bool { role == "眠眠" || role == "user" }
var replies: [FridgeReply] = []
}
struct FridgeReply: Identifiable {
let id: String; let text: String; let role: String; let createdAt: String
var isMine: Bool { role == "眠眠" || role == "user" }
}
func fetch() async {
let req = ServerConfig.makeRequest(path: "fridge/list")
do {
let (data, _) = try await session.data(for: req)
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let items = obj["notes"] as? [[String: Any]] {
notes = items.map { item in
let replies: [FridgeReply] = (item["replies"] as? [[String: Any]] ?? []).map { r in
FridgeReply(id: r["id"] as? String ?? "", text: r["text"] as? String ?? "", role: r["role"] as? String ?? "user", createdAt: r["created_at"] as? String ?? "")
}
return FridgeNote(id: item["id"] as? String ?? "", text: item["text"] as? String ?? "", role: item["role"] as? String ?? "user", createdAt: item["created_at"] as? String ?? "", replies: replies)
}
}
} catch {}
}
func add(text: String) async {
var req = ServerConfig.makeRequest(path: "fridge/add")
req.httpMethod = "POST"; req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: ["text": text, "role": "眠眠"])
_ = try? await session.data(for: req)
await fetch()
}
func reply(noteId: String, text: String) async {
var req = ServerConfig.makeRequest(path: "fridge/reply")
req.httpMethod = "POST"; req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: ["id": noteId, "text": text, "role": "眠眠"])
_ = try? await session.data(for: req)
await fetch()
}
func delete(id: String) async {
var req = ServerConfig.makeRequest(path: "fridge/delete")
req.httpMethod = "POST"; req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: ["id": id])
_ = try? await session.data(for: req)
await fetch()
}
}
// MARK: -
struct FridgePage: View {
@StateObject private var vm = FridgeViewModel()
@State private var showAdd = false
@State private var newText = ""
@State private var selectedNote: FridgeViewModel.FridgeNote?
let theme: Theme
private let size: CGFloat = 42
private let gap: CGFloat = 3
private let cols: [CGFloat] = [32, 77, 122, 167, 212, 257, 302]
var body: some View {
VStack(spacing: 0) {
Spacer()
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .bottom, spacing: 3) {
ForEach((0..<7).reversed(), id: \.self) { dayIndex in
let dayNotes = notesForDay(dayIndex)
VStack(spacing: 3) {
if dayIndex == 6 {
Button { showAdd = true } label: {
Rectangle()
.fill(theme.fridgeAddBtn)
.frame(width: size, height: size)
}
.buttonStyle(.plain)
}
ForEach(dayNotes) { note in
Button {
selectedNote = note
} label: {
Rectangle()
.fill(note.isMine ? theme.fridgeMine : theme.fridgeOther)
.frame(width: size, height: size)
}
.buttonStyle(.plain)
.contextMenu { Button("删除", role: .destructive) { Task { await vm.delete(id: note.id) } } }
}
}
}
}
.padding(.horizontal, 8)
}
.environment(\.layoutDirection, .rightToLeft)
.padding(.bottom, 20)
}
.onAppear { Task { await vm.fetch() } }
.alert("新冰箱贴最多50字", isPresented: $showAdd) {
TextField("内容", text: $newText)
Button("取消", role: .cancel) { newText = "" }
Button("添加") { Task { await vm.add(text: String(newText.prefix(50))); newText = "" } }
}
.sheet(item: $selectedNote) { note in
FridgeDetailView(noteId: note.id, vm: vm, theme: theme)
}
}
private func notesForDay(_ dayIndex: Int) -> [FridgeViewModel.FridgeNote] {
let cal = Calendar.current
let today = cal.startOfDay(for: Date())
guard let targetDay = cal.date(byAdding: .day, value: -(6 - dayIndex), to: today) else { return [] }
let nextDay = cal.date(byAdding: .day, value: 1, to: targetDay)!
return vm.notes.filter { note in
let iso = String(note.createdAt.prefix(19)).replacingOccurrences(of: "T", with: " ")
let fm = DateFormatter(); fm.dateFormat = "yyyy-MM-dd HH:mm:ss"
guard let d = fm.date(from: iso) else { return false }
return d >= targetDay && d < nextDay
}
}
}
struct FridgeNoteCard: View {
let note: FridgeViewModel.FridgeNote
let theme: Theme
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(formattedDate).font(.custom("HYPixel-11px-J", size: 18).weight(.bold)).foregroundStyle(theme.textPrimary)
Text(note.text).font(.custom("HYPixel-11px-J", size: 13).weight(.bold)).foregroundStyle(note.isMine ? theme.fridgeTextMine : theme.fridgeTextOther).lineSpacing(4).lineLimit(2)
if !note.replies.isEmpty {
Text("\(note.replies.count) 条回复").font(.custom("HYPixel-11px-J", size: 11)).foregroundStyle(theme.textTyping)
}
}
.padding(.horizontal, 20).padding(.vertical, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Rectangle().fill(note.isMine ? theme.fridgeMine : theme.fridgeOther))
}
private var formattedDate: String {
String(note.createdAt.prefix(19)).replacingOccurrences(of: "T", with: "-")
}
}
// MARK: -
struct FridgeDetailView: View {
let noteId: String
@ObservedObject var vm: FridgeViewModel
@Environment(\.dismiss) private var dismiss
@State private var replyText = ""
let theme: Theme
private var note: FridgeViewModel.FridgeNote? {
vm.notes.first(where: { $0.id == noteId })
}
var body: some View {
VStack(spacing: 0) {
HStack {
Text(formattedDate).font(.custom("HYPixel-11px-J", size: 16).weight(.bold)).foregroundStyle(theme.textPrimary)
Spacer()
Button("关闭") { dismiss() }.foregroundStyle(theme.textTyping)
}
.padding(.horizontal, 20).padding(.top, 20)
if let note {
Text(note.text)
.font(.custom("HYPixel-11px-J", size: 16))
.foregroundStyle(note.isMine ? theme.fridgeTextMine : theme.fridgeTextOther)
.lineSpacing(6)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(20)
.background(Rectangle().fill(note.isMine ? theme.fridgeMine : theme.fridgeOther))
.padding(.horizontal, 20).padding(.top, 12)
ScrollView {
VStack(spacing: 8) {
ForEach(note.replies) { reply in
HStack {
if reply.isMine { Spacer(minLength: 40) }
VStack(alignment: reply.isMine ? .trailing : .leading, spacing: 2) {
Text(reply.text).font(.custom("HYPixel-11px-J", size: 16))
.foregroundStyle(reply.isMine ? theme.bubbleTextRight : theme.bubbleTextLeft)
.lineSpacing(5)
.padding(.horizontal, 12).padding(.vertical, 6)
.background(Rectangle().fill(reply.isMine ? theme.bubbleRight : theme.bubbleLeft))
Text(replyTime(reply.createdAt)).font(.custom("HYPixel-11px-J", size: 10)).foregroundStyle(theme.textTyping)
}
if !reply.isMine { Spacer(minLength: 40) }
}
.padding(.horizontal, 20)
}
}
.padding(.vertical, 12)
}
HStack(spacing: 0) {
TextField("回复最多50字...", text: $replyText)
.font(.custom("HYPixel-11px-J", size: 16)).foregroundStyle(theme.textPlaceholder)
Spacer()
Button {
guard !replyText.isEmpty else { return }
let t = String(replyText.prefix(50)); replyText = ""
Task { await vm.reply(noteId: noteId, text: t) }
} label: {
Image(systemName: "arrowshape.turn.up.left.fill").font(.system(size: 14)).foregroundStyle(.white)
.frame(width: 32, height: 32).background(Circle().fill(theme.sendBtn))
}
}
.padding(.horizontal, 16).frame(height: 44)
.background(Rectangle().fill(theme.inputBg))
.overlay(Rectangle().stroke(theme.inputBorder, lineWidth: 1))
.padding(.horizontal, 14).padding(.bottom, 8)
}
}
.background(theme.bg.ignoresSafeArea())
}
private var formattedDate: String {
guard let note else { return "" }
return String(note.createdAt.prefix(19)).replacingOccurrences(of: "T", with: "-")
}
private func replyTime(_ iso: String) -> String {
String(iso.prefix(19)).replacingOccurrences(of: "T", with: " ")
}
}