feat(users): 建号规则(手机号必填/随机账号)+手机号绑定表单

管理端与园区端用户创建/编辑表单同步收紧,usersApi 新增 bindPhone

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-30 22:32:49 +08:00
parent 21c4701de9
commit 6568108777
3 changed files with 26 additions and 11 deletions
+2
View File
@@ -33,6 +33,8 @@ export const usersApi = {
api.post<UserRow>(`/admin/users/${id}/status`, { status }),
classify: (id: string, body: { certification_status?: string; affiliation?: string; park_id?: string; park_name?: string; account_type?: string }) =>
api.post<UserRow>(`/admin/users/${id}/classification`, body),
bindPhone: (id: string, phone: string) =>
api.patch<UserRow>(`/admin/users/${id}/phone`, { phone }),
};
// 兼容可能的分页包装 { items, total }
export async function unwrap<T>(p: Promise<T>): Promise<T[]> {
+8 -6
View File
@@ -37,9 +37,10 @@ const GENDERS = [
];
const EMPTY: Record<string, string> = {
username: "", password: "", nickname: "", gender: "", birthday: "", phone: "",
password: "", nickname: "", gender: "", birthday: "", phone: "",
id_card: "", ethnicity: "", grad_school_major: "", grad_time: "", email: "", company: "",
};
const PHONE_RE = /^1[3-9]\d{9}$/;
const genderLabel = (g?: string) => GENDERS.find((x) => x.value === g)?.label || "-";
const CERT_ZH: Record<string, string> = { uncertified: "未认证", pending: "待审", certified: "已认证", rejected: "已驳回" };
@@ -60,8 +61,9 @@ export function UsersTab() {
};
const save = async () => {
if (!form.username.trim()) { window.alert("账号必填"); return; }
if (!PHONE_RE.test(form.phone.trim())) { window.alert("请填写正确的 11 位手机号"); return; }
if (!form.nickname.trim()) { window.alert("姓名必填"); return; }
if (!form.password) { window.alert("请设置初始密码"); return; }
setBusy(true);
try {
await parkApi.createParkUser(form);
@@ -122,11 +124,11 @@ export function UsersTab() {
{/* 新增账号 */}
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="max-h-[85vh] overflow-auto sm:max-w-2xl">
<DialogHeader><DialogTitle></DialogTitle><DialogDescription></DialogDescription></DialogHeader>
<DialogHeader><DialogTitle></DialogTitle><DialogDescription></DialogDescription></DialogHeader>
<ProfileForm form={form} setForm={setForm} includePassword />
<DialogFooter>
<Button variant="outline" onClick={() => setCreateOpen(false)}></Button>
<Button onClick={save} disabled={busy || !form.username.trim() || !form.nickname.trim() || !form.password}></Button>
<Button onClick={save} disabled={busy || !PHONE_RE.test(form.phone.trim()) || !form.nickname.trim() || !form.password}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -170,7 +172,7 @@ function ProfileForm({ form, setForm, includePassword, lockUsername }: { form: R
const set = (k: string, v: string) => setForm({ ...form, [k]: v });
return (
<div className="grid grid-cols-2 gap-4 py-2 md:grid-cols-3">
<Field label="账号 *"><Input value={form.username ?? ""} disabled={lockUsername} onChange={(e) => set("username", e.target.value)} /></Field>
{lockUsername ? <Field label="账号(系统生成,不可改)"><Input value={form.username ?? ""} disabled /></Field> : null}
{includePassword && <Field label="初始密码 *"><Input type="password" value={form.password ?? ""} onChange={(e) => set("password", e.target.value)} /></Field>}
<Field label="姓名 *"><Input value={form.nickname ?? ""} onChange={(e) => set("nickname", e.target.value)} placeholder="姓名(必填)" /></Field>
<Field label="性别">
@@ -180,7 +182,7 @@ function ProfileForm({ form, setForm, includePassword, lockUsername }: { form: R
</Select>
</Field>
<Field label="出生年月"><Input placeholder="YYYY-MM" value={form.birthday ?? ""} onChange={(e) => set("birthday", e.target.value)} /></Field>
<Field label="手机号"><Input value={form.phone ?? ""} onChange={(e) => set("phone", e.target.value)} /></Field>
<Field label="手机号 *"><Input value={form.phone ?? ""} disabled={lockUsername} onChange={(e) => set("phone", e.target.value)} placeholder="11 位手机号(必填)" /></Field>
<Field label="邮箱"><Input value={form.email ?? ""} onChange={(e) => set("email", e.target.value)} /></Field>
<Field label="身份证号"><Input value={form.id_card ?? ""} onChange={(e) => set("id_card", e.target.value)} /></Field>
<Field label="民族"><Input value={form.ethnicity ?? ""} onChange={(e) => set("ethnicity", e.target.value)} /></Field>
+16 -5
View File
@@ -22,9 +22,10 @@ const ACCOUNT_ROLES = [
{ label: "服务方", value: "service", hint: "服务商 / 甲方企业等(统一归服务方)" },
];
const EMPTY = {
username: "", nickname: "", password: "", role: "opc_member",
phone: "", nickname: "", password: "", role: "opc_member",
certification_status: "uncertified", affiliation: "independent", park_id: "", park_name: "",
};
const PHONE_RE = /^1[3-9]\d{9}$/;
const SOURCE_ZH: Record<string, string> = {
mini_program: "小程序", web: "Web站点", desktop: "桌面端", park: "园区",
admin: "管理员", seed: "种子", wx: "微信", phone: "手机号", unknown: "未知",
@@ -45,7 +46,7 @@ export default function UsersPage() {
const openEdit = (r: UserRow) => {
setEditing(r);
setForm({
username: r.username || "", nickname: r.nickname || "", role: r.role || "opc_member",
username: r.username || "", phone: r.phone || "", nickname: r.nickname || "", role: r.role || "opc_member",
certification_status: r.certification_status || "uncertified",
affiliation: r.affiliation || "independent",
park_id: r.park_id || "", park_name: r.park_name || "",
@@ -57,7 +58,7 @@ export default function UsersPage() {
setBusy(true);
try {
if (editing) {
// 丰富编辑:改资料/角色 + 认证/所属/园区
// 丰富编辑:改资料/角色 + 认证/所属/园区 + 绑定手机号
await usersApi.update(editing.id, { nickname: form.nickname, role: form.role });
await usersApi.classify(editing.id, {
certification_status: form.certification_status,
@@ -65,8 +66,11 @@ export default function UsersPage() {
park_id: form.park_id || undefined,
park_name: form.park_name || undefined,
});
if (PHONE_RE.test((form.phone || "").trim()) && form.phone.trim() !== (editing.phone || "")) {
await usersApi.bindPhone(editing.id, form.phone.trim());
}
} else {
await usersApi.create({ username: form.username, nickname: form.nickname, password: form.password || "123456", role: form.role });
await usersApi.create({ phone: form.phone.trim(), nickname: form.nickname, password: form.password || "123456", role: form.role });
}
setOpen(false); res.reload();
} finally { setBusy(false); }
@@ -81,6 +85,7 @@ export default function UsersPage() {
const columns: Column<UserRow>[] = [
{ key: "username", header: "账号", cell: (r) => <span className="font-medium">{r.username}</span> },
{ key: "phone", header: "手机号", cell: (r) => r.phone || "-" },
{ key: "nickname", header: "昵称", cell: (r) => r.nickname || "-" },
{ key: "source", header: "来源", cell: (r) => SOURCE_ZH[r.source ?? ""] || r.source || "-" },
{ key: "certification_status", header: "认证", cell: (r) => CERT_ZH[r.certification_status ?? ""] || r.certification_status || "未认证" },
@@ -132,8 +137,14 @@ export default function UsersPage() {
</div>
)}
<div className="grid grid-cols-2 gap-4 py-2">
<div className="space-y-1.5"><Label></Label><Input disabled={!!editing} value={form.username} onChange={setIn("username")} /></div>
{editing ? (
<div className="space-y-1.5"><Label></Label><Input disabled value={form.username} /></div>
) : null}
{!editing && <div className="space-y-1.5"><Label> *</Label><Input value={form.phone ?? ""} onChange={setIn("phone")} placeholder="11 位手机号(必填,登录凭据)" /></div>}
{!editing && <div className="space-y-1.5"><Label></Label><Input value={form.password ?? ""} onChange={setIn("password")} /></div>}
{editing && (
<div className="space-y-1.5"><Label></Label><Input value={form.phone ?? ""} onChange={setIn("phone")} placeholder="11 位手机号(留空不修改)" /></div>
)}
<div className="space-y-1.5"><Label></Label><Input value={form.nickname} onChange={setIn("nickname")} /></div>
<div className="space-y-1.5"><Label></Label>
<Select value={form.role} onValueChange={set("role")}>