"use client"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { CirclePause, KeyRound, Loader2, Pencil, Plus, RefreshCw, Save, Search, Trash2, UserCheck, UserPlus, UserX } from "lucide-react"; import { crossfadeIn, pulseFeedback, revealChildren, runScopedMotion } from "@/lib/ui/motion"; type OrganizationInfo = { organizationId: string; organizationName: string; }; type OrganizationGroupInfo = { groupId: string; groupName: string; }; type OrganizationRoleInfo = { roleId: string; roleName: string; }; type OrganizationMemberInfo = { memberId: string; memberName: string; memberStatus: string; memberPhone: string; organizationId: string; memberRoleId?: string; memberRoleName?: string; memberGroupId?: string; memberGroupName?: string; memberUserId?: string; }; type PageResult = { records: T[]; total: number; size: number; current: number; pages: number; }; type AccountPayload = { configured: boolean; missing: string[]; staffConfigured: boolean; staffMissing: string[]; selectedOrganizationId: string; organizations: OrganizationInfo[]; groups: OrganizationGroupInfo[]; roles: OrganizationRoleInfo[]; members: PageResult; warnings?: string[]; memberListAvailable?: boolean; passwordManagementAvailable: boolean; }; type MemberForm = { organizationId: string; username: string; memberName: string; memberPhone: string; initialPassword: string; mustChangePassword: boolean; roleId: string; groupId: string; }; type DepartmentForm = { groupName: string; groupDesc: string; }; const blankForm: MemberForm = { organizationId: "", username: "", memberName: "", memberPhone: "", initialPassword: "", mustChangePassword: true, roleId: "", groupId: "" }; const blankDepartmentForm: DepartmentForm = { groupName: "", groupDesc: "" }; const STATUS_OPTIONS = [ { value: "", label: "全部状态" }, { value: "0", label: "在岗" }, { value: "1", label: "休假" }, { value: "2", label: "停用" } ]; export function AccountManager() { const [payload, setPayload] = useState(null); const [organizationId, setOrganizationId] = useState(""); const [status, setStatus] = useState(""); const [query, setQuery] = useState(""); const [draftQuery, setDraftQuery] = useState(""); const [pageNum, setPageNum] = useState(1); const [form, setForm] = useState(blankForm); const [departmentForm, setDepartmentForm] = useState(blankDepartmentForm); const [editingId, setEditingId] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [creatingGroup, setCreatingGroup] = useState(false); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const managerRef = useRef(null); const listRef = useRef(null); const feedbackRef = useRef(null); const selectedOrganizationId = organizationId || payload?.selectedOrganizationId || ""; const members = payload?.members.records || []; const total = payload?.members.total || 0; const pages = payload?.members.pages || 0; const memberListAvailable = payload?.memberListAvailable !== false; const warnings = payload?.warnings || []; const canSubmit = Boolean(payload?.configured && selectedOrganizationId && form.memberName.trim() && (editingId || (payload.staffConfigured && form.memberPhone.trim())) && selectedRoleId(payload, form) && selectedGroupId(payload, form)); const currentOrganization = useMemo(() => { return payload?.organizations.find((item) => item.organizationId === selectedOrganizationId); }, [payload?.organizations, selectedOrganizationId]); useEffect(() => { void loadAccounts(); }, [organizationId, pageNum, query, status]); useEffect(() => { return runScopedMotion(managerRef, (scope) => revealChildren(scope)); }, []); useEffect(() => { crossfadeIn(listRef.current); }, [members.length, pageNum, query, status, selectedOrganizationId]); useEffect(() => { pulseFeedback(feedbackRef.current); }, [error, message]); async function loadAccounts() { setLoading(true); setError(null); try { const params = new URLSearchParams({ pageNum: String(pageNum), pageSize: "10" }); if (organizationId) params.set("organizationId", organizationId); if (query) params.set("memberName", query); if (status) params.set("memberStatus", status); const response = await fetch(`/api/admin/accounts?${params.toString()}`, { cache: "no-store" }); const nextPayload = await response.json() as AccountPayload & { error?: string }; if (!response.ok) throw new Error(nextPayload.error || "读取账号数据失败"); setPayload(nextPayload); syncSelectedOrganization(nextPayload); syncFormDefaults(nextPayload); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } } function syncSelectedOrganization(nextPayload: AccountPayload) { if (!organizationId && nextPayload.selectedOrganizationId) { setOrganizationId(nextPayload.selectedOrganizationId); } } function syncFormDefaults(nextPayload: AccountPayload) { setForm((current) => { const nextOrganizationId = nextPayload.selectedOrganizationId || current.organizationId; return { ...current, organizationId: nextOrganizationId, roleId: nextPayload.roles.some((role) => role.roleId === current.roleId) ? current.roleId : nextPayload.roles[0]?.roleId || "", groupId: nextPayload.groups.some((group) => group.groupId === current.groupId) ? current.groupId : nextPayload.groups[0]?.groupId || "" }; }); } function submitSearch(event: FormEvent) { event.preventDefault(); setPageNum(1); setQuery(draftQuery.trim()); } async function submitMember(event: FormEvent) { event.preventDefault(); setSaving(true); setError(null); setMessage(null); try { const roleId = selectedRoleId(payload, form); const groupId = selectedGroupId(payload, form); const body = editingId ? { memberId: editingId, memberName: form.memberName.trim(), roleId, groupId } : { organizationId: selectedOrganizationId, username: form.username.trim(), phone: form.memberPhone.trim(), name: form.memberName.trim(), initialPassword: form.initialPassword.trim(), mustChangePassword: form.mustChangePassword, memberName: form.memberName.trim(), memberPhone: form.memberPhone.trim(), roleId, groupId }; const response = await fetch("/api/admin/accounts", { method: editingId ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "保存成员失败"); const initialPassword = result.user?.initialPassword; const successMessage = editingId ? "成员信息已更新。" : initialPassword ? `成员已创建,初始密码:${initialPassword}` : "成员已创建。"; setMessage(!editingId && !memberListAvailable ? `${successMessage} 成员列表暂不可用,上游授权后可在列表看到。` : successMessage); resetForm(); await loadAccounts(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function changeStatus(memberId: string, memberStatus: "0" | "1" | "2") { await mutateMember("PUT", { memberId, memberStatus }, "成员状态已更新。"); } async function removeMember(memberId: string) { if (!window.confirm("确定删除这个成员?")) return; await mutateMember("DELETE", { memberId }, "成员已删除。"); } async function resetPassword(member: OrganizationMemberInfo) { if (!member.memberUserId) { setError("该成员还没有绑定企业端用户,不能重置密码。"); return; } const newPassword = window.prompt(`请输入 ${member.memberName} 的新密码`); if (!newPassword) return; setSaving(true); setError(null); setMessage(null); try { const response = await fetch("/api/admin/accounts/password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId: member.memberUserId, newPassword, mustChangePassword: true }) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "重置密码失败"); setMessage("密码已重置,用户下次登录需要修改密码。"); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function mutateMember(method: "PUT" | "DELETE", body: Record, successMessage: string) { setSaving(true); setError(null); setMessage(null); try { const response = await fetch("/api/admin/accounts", { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "成员操作失败"); setMessage(successMessage); await loadAccounts(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function submitGroup(event: FormEvent) { event.preventDefault(); if (!selectedOrganizationId) { setError("请先选择组织。"); return; } const groupName = departmentForm.groupName.trim(); if (!groupName) { setError("部门名称不能为空。"); return; } setCreatingGroup(true); setError(null); setMessage(null); try { const response = await fetch("/api/admin/accounts/groups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ organizationId: selectedOrganizationId, groupName, groupDesc: departmentForm.groupDesc.trim() }) }); const result = await response.json() as { error?: string; group?: OrganizationGroupInfo | null }; if (!response.ok) throw new Error(result.error || "创建部门失败"); setDepartmentForm(blankDepartmentForm); await loadAccounts(); if (result.group?.groupId) { setForm((current) => ({ ...current, groupId: result.group?.groupId || current.groupId })); } setMessage("部门已创建。"); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setCreatingGroup(false); } } function editMember(member: OrganizationMemberInfo) { setEditingId(member.memberId); setForm({ organizationId: member.organizationId || selectedOrganizationId, username: "", memberName: member.memberName || "", memberPhone: member.memberPhone || "", initialPassword: "", mustChangePassword: true, roleId: member.memberRoleId || payload?.roles[0]?.roleId || "", groupId: member.memberGroupId || payload?.groups[0]?.groupId || "" }); } function resetForm() { setEditingId(null); setForm({ organizationId: selectedOrganizationId, username: "", memberName: "", memberPhone: "", initialPassword: "", mustChangePassword: true, roleId: payload?.roles[0]?.roleId || "", groupId: payload?.groups[0]?.groupId || "" }); } return (

账号管理

{payload?.organizations.length || 0} 组织 {total} 成员 {payload?.configured ? "已接入" : "待配置"}
{error || message ? (
{error ?
{error}
: null} {message ?
{message}
: null}
) : null} {warnings.length ? (
{accountWarningText(warnings[0])}
) : null} {!payload?.configured ? (
组织接口未配置:需要组织能力网关地址,用于读取组织、部门、角色和成员列表。
) : !payload.passwordManagementAvailable ? (
企业端用户接口未配置:{payload?.staffMissing.join("、") || "ZHINIAN_STAFF_API_BASE_URL、ZHINIAN_ORG_TENANT_ID"}。配置后可创建企业用户并重置密码。
) : null}

部门管理

{currentOrganization?.organizationName || selectedOrganizationId || "请选择组织"}

{editingId ? "编辑成员" : "新建用户"}

{currentOrganization?.organizationName || selectedOrganizationId || "请选择组织"}

{editingId ? ( ) : null}
{loading && !payload ? (
正在读取账号
) : members.length ? (
成员 角色 / 部门 状态 操作
{members.map((member) => (
{member.memberName} {member.memberPhone || member.memberId}
{member.memberRoleName || "-"} {member.memberGroupName || "-"}
{memberStatusLabel(member.memberStatus)}
))}
) : (
{accountEmptyText(payload)}
)} {pages > 1 ? (
{pageNum} / {pages}
) : null}
); } function selectedRoleId(payload: AccountPayload | null, form: MemberForm): string { if (payload?.roles.some((role) => role.roleId === form.roleId)) return form.roleId; return payload?.roles[0]?.roleId || ""; } function selectedGroupId(payload: AccountPayload | null, form: MemberForm): string { if (payload?.groups.some((group) => group.groupId === form.groupId)) return form.groupId; return payload?.groups[0]?.groupId || ""; } function memberStatusLabel(status: string): string { if (status === "0") return "在岗"; if (status === "1") return "休假"; if (status === "2") return "停用"; return status || "未知"; } function accountEmptyText(payload: AccountPayload | null): string { if (!payload?.configured) return "组织接口配置后显示成员"; if (payload.memberListAvailable === false) return "成员列表暂不可用,新增账号仍可使用"; return "暂无成员"; } function accountWarningText(message: string): string { if (/仅管理员角色|上游.*管理员|hotelStaff 管理员|管理员角色/i.test(message)) { return "成员列表需要认证中心管理员角色 1。当前账号已允许进入本平台后台,但上游 hotelStaff 暂未授权成员列表查询。"; } if (/成员列表|organizationMemberList|ZHINIAN_ORG_MEMBER_LIST_PATH/i.test(message)) { return "成员列表待接入:当前组织服务还没有开放成员查询,新增账号仍可使用。"; } return message; }