import SwiftUI struct AdminMembersTab: AdminPage { @ObservedObject var model: AdminStore @Binding var tab: String @Binding var memberSection: String @Binding var role: String @State private var memberSearch = "" @State private var hours = 168 @State private var uses = 50 @State private var confirmation: AdminAction? var body: some View { members.adminConfirmation($confirmation) } private var filteredMembers: [AdminMember] { model.members.filter { AdminDisplayFilter.matches(memberSearch, fields: [$0.name, $0.userId, memberState($0.status)]) } } private var members: some View { VStack(alignment: .leading, spacing: 16) { HStack { Picker("成员与邀请", selection: $memberSection) { Text("成员列表").tag("成员列表") Text("入组邀请").tag("入组邀请") }.pickerStyle(.segmented).frame(width: 260) Spacer() if let group = model.selectedGroup { Text(group.label).font(.callout).foregroundStyle(.secondary) } } if model.groupID == 0 { VStack(spacing: 12) { emptyState(model.availableGroups.isEmpty ? "当前项目暂无可见组" : "请选择一个管理组", icon: "person.3", message: "在上方选择组别后,即可审核成员或发送入组邀请。") if model.projectID != 0 { Button("查看全部项目的组") { model.projectID = 0 } } if !model.editableProjects.isEmpty { Button("组别管理") { tab = "组别管理" } } if model.access?.platform == true { Button("项目与授权") { tab = "项目与授权" } } }.frame(maxWidth: .infinity, maxHeight: .infinity) } else if let group = model.selectedGroup, !group.canManage { VStack(spacing: 12) { emptyState("\(group.label) · 未授权", icon: "lock", message: "当前账户没有本组成员管理权限") if model.access?.platform == true { Button("项目负责人授权") { model.projectID = group.projectId role = "leader" tab = "项目与授权" } } }.frame(maxWidth: .infinity, maxHeight: .infinity) } else { if memberSection == "成员列表" { memberList } else { invitationPanel } } }.padding(20) } private var memberList: some View { VStack(alignment: .leading, spacing: 14) { HStack { searchField("搜索姓名、账户或状态", text: $memberSearch).frame(maxWidth: 340) Spacer() Text("已加载 \(model.members.count) 人 · 待审核 \(model.members.filter { $0.status == "pending" }.count) 人") .font(.callout).foregroundStyle(.secondary) Button { memberSection = "入组邀请" } label: { Label("邀请成员", systemImage: "person.badge.plus") }.buttonStyle(.borderedProminent) } ZStack { Table(filteredMembers) { TableColumn("姓名", value: \.name) TableColumn("账户 ID", value: \.userId) TableColumn("状态") { statusBadge(memberState($0.status), positive: $0.status == "active", pending: $0.status == "pending") } TableColumn("操作") { m in HStack { if m.status == "pending" { Button("批准入组") { run { try await model.reviewMember(m, status: "active") } } Button("拒绝") { confirm("拒绝 \(m.name) 的入组申请?") { try await model.reviewMember(m, status: "rejected") } } } else if m.status == "active" { Button("停用") { confirm("停用 \(m.name) 的本组成员资格?") { try await model.reviewMember(m, status: "disabled") } } } else { Button("恢复") { confirm("恢复 \(m.name) 的本组成员资格?") { try await model.reviewMember(m, status: "active") } } } } }.width(min: 200) }.frame(minHeight: 180).clipShape(RoundedRectangle(cornerRadius: 10)) if filteredMembers.isEmpty { emptyState("暂无匹配成员", icon: "person.2", message: "可以清空搜索,或通过邀请二维码让成员申请入组。") .allowsHitTesting(false) } } Text("成员资格与管理员权限独立;停用、恢复等操作仅作用于当前组。") .font(.caption).foregroundStyle(.secondary) } } private var invitationPanel: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { sectionCard("创建入组邀请", icon: "qrcode") { Text("设置有效期和人数上限,生成海报后发给成员。扫码申请仍需管理员审核。") .font(.callout).foregroundStyle(.secondary) HStack(spacing: 24) { Stepper("有效期 \(hours) 小时", value: $hours, in: 1...720).frame(width: 240) Stepper("最多 \(uses) 人", value: $uses, in: 1...1000).frame(width: 200) Spacer() } HStack { Button { run { try await model.createInvite(hours: hours, uses: uses) } } label: { Label("生成邀请海报", systemImage: "qrcode") }.buttonStyle(.borderedProminent) Button { run { try await model.retryInviteQR() } } label: { Label("重试加载二维码", systemImage: "arrow.clockwise") } } if let poster = model.invitationPoster { HStack { Image(nsImage: poster).resizable().interpolation(.high).scaledToFit().frame(width: 210, height: 280) VStack(alignment: .leading, spacing: 10) { Text("邀请海报已生成").font(.headline) Text("二维码和有效期已经排版到海报中,可直接保存后发送给成员。").foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) Button { saveQR(poster, filename: "报销组入组邀请海报.png") } label: { Label("保存邀请海报", systemImage: "square.and.arrow.down") }.buttonStyle(.borderedProminent) Button { run { try await model.retryInviteQR() } } label: { Label("重新生成海报", systemImage: "arrow.clockwise") } }.frame(maxWidth: 380, alignment: .leading) } } } sectionCard("邀请记录", icon: "clock") { if model.invites.isEmpty { Text("还没有邀请记录").foregroundStyle(.secondary) } ForEach(model.invites) { invite in HStack(spacing: 16) { VStack(alignment: .leading, spacing: 5) { Text("邀请 #\(invite.id) · 已使用 \(invite.uses) / \(invite.maxUses) 人").font(.callout.weight(.medium)) Text("有效期至 \(invite.expiresAt)").font(.caption).foregroundStyle(.secondary).textSelection(.enabled) } Spacer() if invite.active { Button("撤销邀请", role: .destructive) { confirm("撤销邀请 #\(invite.id)?") { try await model.revoke(invite) } } } else { statusBadge("已撤销") } }.padding(.vertical, 6) Divider() } } }.frame(maxWidth: 1060).frame(maxWidth: .infinity) } } private func confirm(_ title: String, _ work: @escaping () async throws -> Void) { confirmation = AdminAction(title: title) { run(work) } } }