feat: allow admins to bind member phone numbers

Members page previously only displayed phone status; add bind/replace UI and admin member update phone support for login and sync identity.
This commit is contained in:
whm
2026-08-05 14:41:27 +08:00
parent cb56e6847e
commit 3c9fe46359
4 changed files with 119 additions and 9 deletions

View File

@@ -38,6 +38,9 @@ export function MembersPage(props: {
const [orgs, setOrgs] = useState<OrgUnit[]>([]);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [phoneOpen, setPhoneOpen] = useState(false);
const [phoneTarget, setPhoneTarget] = useState<Member | null>(null);
const [phoneValue, setPhoneValue] = useState("");
const [form] = Form.useForm<{
password?: string;
display_name?: string;
@@ -120,6 +123,7 @@ export function MembersPage(props: {
>
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
/
{session.role === "超级管理员" || session.role === "platform_admin"
? " 超管代管修改时会要求两次确认。"
: ""}
@@ -134,8 +138,28 @@ export function MembersPage(props: {
{
title: "手机号",
dataIndex: "phone",
width: 120,
render: (v: string) => v || <Typography.Text type="secondary"></Typography.Text>,
width: 200,
render: (v: string, r: Member) => (
<Space size={4}>
{v ? (
<Typography.Text copyable>{v}</Typography.Text>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
<Button
type="link"
size="small"
style={{ padding: 0 }}
onClick={() => {
setPhoneTarget(r);
setPhoneValue(v || "");
setPhoneOpen(true);
}}
>
{v ? "更换" : "绑定"}
</Button>
</Space>
),
},
{ title: "显示名", dataIndex: "display_name", width: 120 },
{
@@ -204,6 +228,54 @@ export function MembersPage(props: {
]}
/>
<Modal
title={phoneTarget?.phone ? "更换手机号" : "绑定手机号"}
open={phoneOpen}
onCancel={() => {
setPhoneOpen(false);
setPhoneTarget(null);
}}
onOk={async () => {
if (!phoneTarget) return;
const trimmed = phoneValue.trim();
const ok = await confirmCompanyWrite(
modal,
session,
trimmed ? "绑定/更换成员手机号" : "解绑成员手机号",
`成员:${phoneTarget.display_name || phoneTarget.username}#${phoneTarget.user_id})→ ${trimmed || "(空)"}`
);
if (!ok) return;
setBusy(true);
try {
await updateMember(session, phoneTarget.user_id, { phone: trimmed });
setInfo(trimmed ? `已绑定 ${trimmed}` : "已解绑手机号");
setPhoneOpen(false);
setPhoneTarget(null);
await refresh();
} catch (e: any) {
message.error(e.message || String(e));
} finally {
setBusy(false);
}
}}
confirmLoading={busy}
destroyOnHidden
okText="保存"
>
<Typography.Paragraph type="secondary">
线{" "}
<Typography.Text code>13531041944</Typography.Text>
13531041945
</Typography.Paragraph>
<Input
value={phoneValue}
onChange={(e) => setPhoneValue(e.target.value)}
placeholder="例13531041944"
maxLength={20}
allowClear
/>
</Modal>
<Modal
title="新建成员"
open={open}

View File

@@ -1557,7 +1557,7 @@ export async function bindPhone(
export async function updateMember(
session: Session,
id: number,
body: { role?: string; org_unit_id?: number; status?: string }
body: { role?: string; org_unit_id?: number; status?: string; phone?: string }
) {
const res = await apiFetch(`/api/v1/admin/members/${id}`, {
method: "PUT",