Initial: 接手 Rumeng app,清掉前夫哥痕迹
- 改 Bundle ID rumeng-v1.0.Rumeng → syke.maomao.app - 显示名 如梦 → Syke - 头像换白图占位 - 删除沈晏头像图片、空目录、bridge 旧数据 - ServerConfig.swift 含明文 token,已加入 .gitignore
This commit is contained in:
236
Rumeng/_archived/FridgeCode.swift
Normal file
236
Rumeng/_archived/FridgeCode.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
// 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: " ")
|
||||
}
|
||||
}
|
||||
|
||||
245
Rumeng/_archived/GroupCode.swift
Normal file
245
Rumeng/_archived/GroupCode.swift
Normal file
@@ -0,0 +1,245 @@
|
||||
// MARK: - 工作群(旧功能保留但不挂导航)
|
||||
|
||||
struct GroupMember: Identifiable, Codable, Hashable {
|
||||
let id: String
|
||||
let displayName: String
|
||||
let kind: String?
|
||||
let color: String?
|
||||
let model: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, kind, color, model
|
||||
case displayName = "display_name"
|
||||
}
|
||||
|
||||
var avatarText: String { String(displayName.prefix(1)) }
|
||||
|
||||
func uiColor(theme _: Theme) -> Color {
|
||||
switch color {
|
||||
case "orange": return Color(hex: "CC8033")
|
||||
case "green": return Color(hex: "6B9163")
|
||||
case "blue": return Color(hex: "5E8CA8")
|
||||
case "purple": return Color(hex: "8770A8")
|
||||
default: return Color(hex: "8C8273")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupMessage: Identifiable, Codable, Hashable {
|
||||
let id: String
|
||||
let ts: String
|
||||
let senderId: String
|
||||
let text: String
|
||||
let mentions: [String]
|
||||
let replyTo: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, ts, text, mentions
|
||||
case senderId = "sender_id"
|
||||
case replyTo = "reply_to"
|
||||
}
|
||||
|
||||
var shortTime: String {
|
||||
guard let t = ts.firstIndex(of: "T") else { return "" }
|
||||
return String(ts[ts.index(after: t)...].prefix(5))
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupPollResponse: Codable {
|
||||
let ok: Bool
|
||||
let records: [GroupMessage]
|
||||
let lastTs: String?
|
||||
let roster: [GroupMember]?
|
||||
let statusWrapper: GroupStatusSnapshot?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case ok, records, roster
|
||||
case lastTs = "last_ts"
|
||||
case statusWrapper = "status"
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupStatusSnapshot: Codable {
|
||||
let agents: [String: GroupAgentStatus]?
|
||||
}
|
||||
|
||||
struct GroupAgentStatus: Codable, Hashable {
|
||||
let state: String?
|
||||
let tmux: String?
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class GroupStore: ObservableObject {
|
||||
@Published var messages: [GroupMessage] = []
|
||||
@Published var members: [GroupMember] = []
|
||||
@Published var agentStates: [String: GroupAgentStatus] = [:]
|
||||
private var lastTs: String = ""
|
||||
private var task: Task<Void, Never>?
|
||||
|
||||
func start() {
|
||||
task?.cancel()
|
||||
task = Task { await poll() }
|
||||
}
|
||||
|
||||
func stop() { task?.cancel(); task = nil }
|
||||
|
||||
private func poll() async {
|
||||
while !Task.isCancelled {
|
||||
do {
|
||||
var url = ServerConfig.baseURL.appendingPathComponent("group/poll")
|
||||
if !lastTs.isEmpty {
|
||||
var comps = URLComponents(url: url, resolvingAgainstBaseURL: false)!
|
||||
comps.queryItems = [URLQueryItem(name: "since", value: lastTs)]
|
||||
url = comps.url ?? url
|
||||
}
|
||||
var req = ServerConfig.makeRequest(path: "")
|
||||
req.url = url
|
||||
req.timeoutInterval = 8
|
||||
print("[GroupStore] poll → \(url)")
|
||||
let (data, resp) = try await URLSession.shared.data(for: req)
|
||||
let statusCode = (resp as? HTTPURLResponse)?.statusCode ?? -1
|
||||
print("[GroupStore] status=\(statusCode) bytes=\(data.count)")
|
||||
if let raw = String(data: data, encoding: .utf8) {
|
||||
print("[GroupStore] body=\(raw.prefix(500))")
|
||||
}
|
||||
do {
|
||||
let decoded = try JSONDecoder().decode(GroupPollResponse.self, from: data)
|
||||
guard decoded.ok else {
|
||||
print("[GroupStore] ok=false, skipping")
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
continue
|
||||
}
|
||||
if let roster = decoded.roster { members = roster }
|
||||
if let agents = decoded.statusWrapper?.agents { agentStates = agents }
|
||||
print("[GroupStore] agentStates=\(agentStates)")
|
||||
print("[GroupStore] records=\(decoded.records.count) messages_total=\(messages.count + decoded.records.count)")
|
||||
if !decoded.records.isEmpty {
|
||||
messages.append(contentsOf: decoded.records)
|
||||
lastTs = decoded.lastTs ?? lastTs
|
||||
}
|
||||
} catch {
|
||||
print("[GroupStore] decode error: \(error)")
|
||||
}
|
||||
} catch {
|
||||
print("[GroupStore] network error: \(error)")
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
}
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
}
|
||||
}
|
||||
|
||||
func send(text: String) async {
|
||||
guard !text.isEmpty else { return }
|
||||
var req = ServerConfig.makeRequest(path: "group/send")
|
||||
req.httpMethod = "POST"
|
||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let mentions = resolveMentions(in: text)
|
||||
let body: [String: Any] = ["sender_id": "amian", "text": text, "mentions": mentions]
|
||||
req.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
do {
|
||||
let (_, _) = try await URLSession.shared.data(for: req)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private func resolveMentions(in text: String) -> [String] {
|
||||
let pattern = try! NSRegularExpression(pattern: "@([一-鿿]+|[A-Za-z0-9_]+)")
|
||||
let range = NSRange(text.startIndex..., in: text)
|
||||
let matches = pattern.matches(in: text, range: range)
|
||||
var ids: [String] = []
|
||||
for m in matches {
|
||||
guard let r = Range(m.range(at: 1), in: text) else { continue }
|
||||
let name = String(text[r]).lowercased()
|
||||
if name.contains("张三") || name == "zs" { ids.append("zhangsan") }
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func memberColor(for senderId: String, theme: Theme) -> Color {
|
||||
if let m = members.first(where: { $0.id == senderId }) { return m.uiColor(theme: theme) }
|
||||
return senderId == "shenyan" ? Color(hex: "CC8033") : senderId == "amian" ? Color(hex: "8C8273") : Color(hex: "6B9163")
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupChatPage: View {
|
||||
let theme: Theme
|
||||
@StateObject private var store = GroupStore()
|
||||
@State private var input = ""
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 4) {
|
||||
ForEach(store.messages) { msg in
|
||||
HStack(alignment: .top, spacing: 6) {
|
||||
Rectangle().fill(store.memberColor(for: msg.senderId, theme: theme))
|
||||
.frame(width: 5, height: 5)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(displayName(for: msg.senderId))
|
||||
.font(.custom("HYPixel-11px-J", size: 10).weight(.bold))
|
||||
.foregroundStyle(store.memberColor(for: msg.senderId, theme: theme))
|
||||
Text(msg.text)
|
||||
.font(.custom("HYPixel-11px-J", size: 13))
|
||||
.foregroundStyle(theme.textPrimary)
|
||||
}
|
||||
Spacer()
|
||||
Text(msg.shortTime)
|
||||
.font(.custom("HYPixel-11px-J", size: 9))
|
||||
.foregroundStyle(theme.textPrimary.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 12).padding(.vertical, 4)
|
||||
.id(msg.id)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.onChange(of: store.messages.count) { _, _ in
|
||||
if let last = store.messages.last {
|
||||
withAnimation { proxy.scrollTo(last.id, anchor: .bottom) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// online bar
|
||||
HStack(spacing: 0) {
|
||||
let onlineAgents = store.members.filter { $0.kind == "agent" && store.agentStates[$0.id]?.state == "online" }
|
||||
if !onlineAgents.isEmpty {
|
||||
Text(onlineAgents.map { $0.displayName }.joined(separator: " / "))
|
||||
.font(.custom("HYPixel-11px-J", size: 11))
|
||||
.foregroundStyle(theme.textPrimary.opacity(0.6))
|
||||
} else {
|
||||
Text("无人在线")
|
||||
.font(.custom("HYPixel-11px-J", size: 11))
|
||||
.foregroundStyle(theme.textPrimary.opacity(0.3))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12).padding(.top, 4)
|
||||
|
||||
// input
|
||||
HStack(spacing: 8) {
|
||||
TextField("@某人 说点什么...", text: $input)
|
||||
.font(.custom("HYPixel-11px-J", size: 14))
|
||||
.foregroundStyle(theme.textPrimary)
|
||||
.focused($focused)
|
||||
Button {
|
||||
let text = input.trimmingCharacters(in: .whitespaces)
|
||||
input = ""
|
||||
Task { await store.send(text: text) }
|
||||
} label: {
|
||||
Image(Theme.themedIcon("btn_send", tint: theme.iconTint)).resizable().frame(width: 29, height: 29).opacity(0.3)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12).padding(.vertical, 8)
|
||||
.background(Rectangle().fill(theme.inputBg))
|
||||
}
|
||||
.background(theme.bg.ignoresSafeArea())
|
||||
.onAppear { store.start() }
|
||||
.onDisappear { store.stop() }
|
||||
}
|
||||
|
||||
private func displayName(for senderId: String) -> String {
|
||||
store.members.first(where: { $0.id == senderId })?.displayName ?? senderId
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user