641 lines
25 KiB
TypeScript
641 lines
25 KiB
TypeScript
"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<T> = {
|
||
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<OrganizationMemberInfo>;
|
||
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<AccountPayload | null>(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<MemberForm>(blankForm);
|
||
const [departmentForm, setDepartmentForm] = useState<DepartmentForm>(blankDepartmentForm);
|
||
const [editingId, setEditingId] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [creatingGroup, setCreatingGroup] = useState(false);
|
||
const [message, setMessage] = useState<string | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const managerRef = useRef<HTMLDivElement | null>(null);
|
||
const listRef = useRef<HTMLElement | null>(null);
|
||
const feedbackRef = useRef<HTMLDivElement | null>(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<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
setPageNum(1);
|
||
setQuery(draftQuery.trim());
|
||
}
|
||
|
||
async function submitMember(event: FormEvent<HTMLFormElement>) {
|
||
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<string, string>, 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<HTMLFormElement>) {
|
||
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 (
|
||
<div className="account-manager" ref={managerRef}>
|
||
<div className="workspace-head" data-animate>
|
||
<div>
|
||
<h1 className="workspace-title">账号管理</h1>
|
||
</div>
|
||
<div className="workspace-meta">
|
||
<span className="status">{payload?.organizations.length || 0} 组织</span>
|
||
<span className="status running">{total} 成员</span>
|
||
<span className={`status ${payload?.configured ? "" : "failed"}`}>{payload?.configured ? "已接入" : "待配置"}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<section className="panel account-actions" data-animate>
|
||
<label className="field account-org-field">
|
||
<span>组织</span>
|
||
<select
|
||
value={selectedOrganizationId}
|
||
onChange={(event) => {
|
||
setOrganizationId(event.target.value);
|
||
setPageNum(1);
|
||
setEditingId(null);
|
||
setForm({ ...blankForm, organizationId: event.target.value });
|
||
}}
|
||
disabled={!payload?.configured || loading}
|
||
>
|
||
{payload?.organizations.length ? payload.organizations.map((item) => (
|
||
<option value={item.organizationId} key={item.organizationId}>{item.organizationName || item.organizationId}</option>
|
||
)) : (
|
||
<option value="">{payload?.configured ? "暂无组织" : "组织接口未配置"}</option>
|
||
)}
|
||
</select>
|
||
</label>
|
||
<form className="log-search account-search" onSubmit={submitSearch}>
|
||
<Search aria-hidden="true" />
|
||
<input
|
||
value={draftQuery}
|
||
placeholder="搜索成员姓名"
|
||
onChange={(event) => setDraftQuery(event.target.value)}
|
||
disabled={!memberListAvailable || loading}
|
||
/>
|
||
</form>
|
||
<select className="account-status-select" value={status} onChange={(event) => {
|
||
setStatus(event.target.value);
|
||
setPageNum(1);
|
||
}} disabled={!memberListAvailable || loading}>
|
||
{STATUS_OPTIONS.map((item) => <option value={item.value} key={item.value}>{item.label}</option>)}
|
||
</select>
|
||
<div className="toolbar">
|
||
<button className="button" type="button" onClick={loadAccounts} disabled={loading || saving}>
|
||
{loading ? <Loader2 className="spin" size={18} /> : <RefreshCw size={18} />}
|
||
刷新
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
{error || message ? (
|
||
<div ref={feedbackRef}>
|
||
{error ? <div className="callout" role="alert">{error}</div> : null}
|
||
{message ? <div className="callout success-callout" role="status" aria-live="polite">{message}</div> : null}
|
||
</div>
|
||
) : null}
|
||
|
||
{warnings.length ? (
|
||
<div className="callout" role="status" data-animate>
|
||
{accountWarningText(warnings[0])}
|
||
</div>
|
||
) : null}
|
||
|
||
{!payload?.configured ? (
|
||
<div className="callout" role="status" data-animate>
|
||
组织接口未配置:需要组织能力网关地址,用于读取组织、部门、角色和成员列表。
|
||
</div>
|
||
) : !payload.passwordManagementAvailable ? (
|
||
<div className="callout" role="status" data-animate>
|
||
企业端用户接口未配置:{payload?.staffMissing.join("、") || "ZHINIAN_STAFF_API_BASE_URL、ZHINIAN_ORG_TENANT_ID"}。配置后可创建企业用户并重置密码。
|
||
</div>
|
||
) : null}
|
||
|
||
<section className="panel account-department-panel" data-animate>
|
||
<div className="settings-group-head">
|
||
<div>
|
||
<h2>部门管理</h2>
|
||
<p className="muted">{currentOrganization?.organizationName || selectedOrganizationId || "请选择组织"}</p>
|
||
</div>
|
||
</div>
|
||
<form className="account-department-form" onSubmit={submitGroup}>
|
||
<label className="field">
|
||
<span>部门名称</span>
|
||
<input
|
||
value={departmentForm.groupName}
|
||
onChange={(event) => setDepartmentForm((item) => ({ ...item, groupName: event.target.value }))}
|
||
disabled={!payload?.configured || !selectedOrganizationId || creatingGroup}
|
||
/>
|
||
</label>
|
||
<label className="field">
|
||
<span>部门描述</span>
|
||
<input
|
||
value={departmentForm.groupDesc}
|
||
placeholder="可留空"
|
||
onChange={(event) => setDepartmentForm((item) => ({ ...item, groupDesc: event.target.value }))}
|
||
disabled={!payload?.configured || !selectedOrganizationId || creatingGroup}
|
||
/>
|
||
</label>
|
||
<button className="button" type="submit" disabled={!payload?.configured || !selectedOrganizationId || creatingGroup || !departmentForm.groupName.trim()}>
|
||
{creatingGroup ? <Loader2 className="spin" size={18} /> : <Plus size={18} />}
|
||
创建部门
|
||
</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section className="panel account-form-panel" data-animate>
|
||
<div className="settings-group-head">
|
||
<div>
|
||
<h2>{editingId ? "编辑成员" : "新建用户"}</h2>
|
||
<p className="muted">{currentOrganization?.organizationName || selectedOrganizationId || "请选择组织"}</p>
|
||
</div>
|
||
{editingId ? (
|
||
<button className="button" type="button" onClick={resetForm} disabled={saving}>
|
||
取消编辑
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
<form className="account-form-grid" onSubmit={submitMember}>
|
||
<label className="field">
|
||
<span>成员名称</span>
|
||
<input value={form.memberName} onChange={(event) => setForm((item) => ({ ...item, memberName: event.target.value }))} disabled={!payload?.configured || saving} />
|
||
</label>
|
||
<label className="field">
|
||
<span>手机号</span>
|
||
<input value={form.memberPhone} onChange={(event) => setForm((item) => ({ ...item, memberPhone: event.target.value }))} disabled={!payload?.configured || saving || Boolean(editingId)} />
|
||
</label>
|
||
<label className="field">
|
||
<span>用户名</span>
|
||
<input value={form.username} placeholder="默认使用手机号" onChange={(event) => setForm((item) => ({ ...item, username: event.target.value }))} disabled={!payload?.staffConfigured || saving || Boolean(editingId)} />
|
||
</label>
|
||
<label className="field">
|
||
<span>初始密码</span>
|
||
<input type="password" value={form.initialPassword} placeholder="留空由服务端生成" onChange={(event) => setForm((item) => ({ ...item, initialPassword: event.target.value }))} disabled={!payload?.staffConfigured || saving || Boolean(editingId)} />
|
||
</label>
|
||
<label className="field">
|
||
<span>角色</span>
|
||
<select value={selectedRoleId(payload, form)} onChange={(event) => setForm((item) => ({ ...item, roleId: event.target.value }))} disabled={!payload?.roles.length || saving}>
|
||
{payload?.roles.length ? payload.roles.map((role) => (
|
||
<option value={role.roleId} key={role.roleId}>{role.roleName}</option>
|
||
)) : <option value="">暂无角色</option>}
|
||
</select>
|
||
</label>
|
||
<label className="field">
|
||
<span>部门</span>
|
||
<select value={selectedGroupId(payload, form)} onChange={(event) => setForm((item) => ({ ...item, groupId: event.target.value }))} disabled={!payload?.groups.length || saving}>
|
||
{payload?.groups.length ? payload.groups.map((group) => (
|
||
<option value={group.groupId} key={group.groupId}>{group.groupName}</option>
|
||
)) : <option value="">暂无部门</option>}
|
||
</select>
|
||
</label>
|
||
<label className="toggle-line account-password-toggle">
|
||
<input
|
||
type="checkbox"
|
||
checked={form.mustChangePassword}
|
||
onChange={(event) => setForm((item) => ({ ...item, mustChangePassword: event.target.checked }))}
|
||
disabled={!payload?.staffConfigured || saving || Boolean(editingId)}
|
||
/>
|
||
首次登录改密
|
||
</label>
|
||
<div className="toolbar account-form-submit">
|
||
<button className="button primary account-create-submit" type="submit" disabled={!canSubmit || saving || creatingGroup}>
|
||
{saving ? <Loader2 className="spin" size={18} /> : editingId ? <Save size={18} /> : <UserPlus size={18} />}
|
||
{editingId ? "保存成员" : "新建用户"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
|
||
<section className="panel account-list" data-animate ref={listRef}>
|
||
{loading && !payload ? (
|
||
<div className="settings-loading">
|
||
<Loader2 className="spin" size={18} />
|
||
正在读取账号
|
||
</div>
|
||
) : members.length ? (
|
||
<div className="account-table" role="table" aria-label="账号成员">
|
||
<div className="account-row account-head" role="row">
|
||
<span>成员</span>
|
||
<span>角色 / 部门</span>
|
||
<span>状态</span>
|
||
<span>操作</span>
|
||
</div>
|
||
{members.map((member) => (
|
||
<div className="account-row" role="row" key={member.memberId}>
|
||
<div className="account-member-main">
|
||
<strong>{member.memberName}</strong>
|
||
<small>{member.memberPhone || member.memberId}</small>
|
||
</div>
|
||
<div className="account-member-main">
|
||
<strong>{member.memberRoleName || "-"}</strong>
|
||
<small>{member.memberGroupName || "-"}</small>
|
||
</div>
|
||
<span className={`status ${member.memberStatus === "2" ? "failed" : member.memberStatus === "1" ? "running" : ""}`}>{memberStatusLabel(member.memberStatus)}</span>
|
||
<div className="toolbar account-row-actions">
|
||
<button className="icon-button" type="button" title="编辑成员" aria-label="编辑成员" onClick={() => editMember(member)} disabled={saving}>
|
||
<Pencil aria-hidden="true" />
|
||
</button>
|
||
<button className="icon-button" type="button" title="重置密码" aria-label="重置密码" onClick={() => resetPassword(member)} disabled={saving || !payload?.staffConfigured || !member.memberUserId}>
|
||
<KeyRound aria-hidden="true" />
|
||
</button>
|
||
<button className="icon-button" type="button" title={member.memberStatus === "0" ? "设为休假" : "设为在岗"} aria-label={member.memberStatus === "0" ? "设为休假" : "设为在岗"} onClick={() => changeStatus(member.memberId, member.memberStatus === "0" ? "1" : "0")} disabled={saving}>
|
||
{member.memberStatus === "0" ? <CirclePause aria-hidden="true" /> : <UserCheck aria-hidden="true" />}
|
||
</button>
|
||
<button className="icon-button danger-icon" type="button" title="停用成员" aria-label="停用成员" onClick={() => changeStatus(member.memberId, "2")} disabled={saving || member.memberStatus === "2"}>
|
||
<UserX aria-hidden="true" />
|
||
</button>
|
||
<button className="icon-button danger-icon" type="button" title="删除成员" aria-label="删除成员" onClick={() => removeMember(member.memberId)} disabled={saving}>
|
||
<Trash2 aria-hidden="true" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="log-empty">{accountEmptyText(payload)}</div>
|
||
)}
|
||
{pages > 1 ? (
|
||
<div className="pagination">
|
||
<button className="icon-button" type="button" onClick={() => setPageNum((item) => Math.max(1, item - 1))} disabled={pageNum <= 1 || loading}>‹</button>
|
||
<span className="pager-meta">{pageNum} / {pages}</span>
|
||
<button className="icon-button" type="button" onClick={() => setPageNum((item) => Math.min(pages, item + 1))} disabled={pageNum >= pages || loading}>›</button>
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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;
|
||
}
|