feat(users): 建号规则(手机号必填/随机账号)+手机号绑定表单
管理端与园区端用户创建/编辑表单同步收紧,usersApi 新增 bindPhone Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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[]> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
Reference in New Issue
Block a user