143 lines
8.4 KiB
Swift
143 lines
8.4 KiB
Swift
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) }
|
||
}
|
||
}
|