R84 R86 收口用户开通界面与自动化回归

这个提交包含在:
2026-05-05 00:06:12 +08:00
父节点 d791357ca2
当前提交 6f22dbb1c4
共修改 10 个文件,包含 738 行新增和 46 行删除
+1 -3
查看文件
@@ -88,16 +88,14 @@ export async function fetchUsers(params: {
}
export async function createUser(payload: {
employeeNo: string;
name: string;
phone: string;
email: string;
password: string;
departmentId?: string;
directManagerId?: string | null;
roles: string[];
}) {
return apiRequestData<UserSummary>("/api/users", {
return apiRequestData<UserSummary & { oneTimePassword: string }>("/api/users", {
method: "POST",
body: JSON.stringify(payload),
});
+3 -3
查看文件
@@ -110,8 +110,8 @@ export function LoginPage() {
<h2 className="mt-2 text-xl font-semibold tracking-tight text-slate-900">使用账号密码登录</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
{primary
? "适合桌面浏览器直接登录;若企业微信入口暂未开放,也可正常使用。"
: "企业微信免登失败、未绑定或超时时,可改用账号密码继续进入。"}
? "适合桌面浏览器直接登录;若是首次开通,也可使用管理员转交的一次性口令进入。"
: "企业微信免登失败、未绑定或超时时,可改用账号密码或一次性口令继续进入。"}
</p>
</div>
@@ -135,7 +135,7 @@ export function LoginPage() {
data-testid="login-password"
id="login-password"
onChange={(event) => setPassword(event.target.value)}
placeholder="请输入密码"
placeholder="请输入密码或一次性口令"
type="password"
value={password}
/>
+1 -1
查看文件
@@ -112,7 +112,7 @@ export function ProfilePage() {
<dd className="mt-1 font-medium text-zinc-900">{me?.name ?? "-"}</dd>
</div>
<div>
<dt className="text-zinc-500">员工号</dt>
<dt className="text-zinc-500">员工号(系统生成)</dt>
<dd className="mt-1 font-medium text-zinc-900">{me?.employeeNo ?? "-"}</dd>
</div>
<div>
+29 -6
查看文件
@@ -49,7 +49,12 @@ export function UserManagementPage() {
const [q, setQ] = useState("");
const [page, setPage] = useState(1);
const [error, setError] = useState("");
const [resetResult, setResetResult] = useState<{ userId: string; password: string } | null>(null);
const [credentialResult, setCredentialResult] = useState<{
mode: "create" | "reset";
userId: string;
employeeNo?: string;
password: string;
} | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [importOpen, setImportOpen] = useState(false);
@@ -74,10 +79,16 @@ export function UserManagementPage() {
const createMutation = useMutation({
mutationFn: (payload: CreateUserFormData) => createUser(payload),
onSuccess: async () => {
onSuccess: async (data) => {
await queryClient.invalidateQueries({ queryKey: ["users"] });
setCreateOpen(false);
setError("");
setCredentialResult({
mode: "create",
userId: data.id,
employeeNo: data.employeeNo,
password: data.oneTimePassword,
});
toast.success("用户已创建");
},
onError: (err) => {
@@ -109,7 +120,7 @@ export function UserManagementPage() {
const resetMutation = useMutation({
mutationFn: async (item: UserSummary) => resetUserPassword(item.id),
onSuccess: async (data) => {
setResetResult({ userId: data.id, password: data.oneTimePassword });
setCredentialResult({ mode: "reset", userId: data.id, password: data.oneTimePassword });
await queryClient.invalidateQueries({ queryKey: ["users"] });
setError("");
toast.success("密码已重置");
@@ -142,7 +153,7 @@ export function UserManagementPage() {
<div className="space-y-6">
<PageHeader
title="系统用户管理"
subtitle="创建/启用/禁用账号,重置密码并分配角色(仅试运行最小运营)。"
subtitle="创建/启用/禁用账号,系统自动生成工号并展示一次性口令,管理员只负责最小开通与角色分配。"
actions={
<div className="flex gap-2">
<UserImportDialog
@@ -184,9 +195,21 @@ export function UserManagementPage() {
</p>
) : null}
{resetResult ? (
{credentialResult ? (
<p className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800" data-testid="users-reset-result">
已重置密码(user={resetResult.userId}),一次性口令:<span className="font-mono">{resetResult.password}</span>
{credentialResult.mode === "create"
? (
<>
已创建用户(user={credentialResult.userId}
{credentialResult.employeeNo ? `,工号=${credentialResult.employeeNo}` : ""}
),一次性口令:<span className="font-mono">{credentialResult.password}</span>
</>
)
: (
<>
已重置密码(user={credentialResult.userId}),一次性口令:<span className="font-mono">{credentialResult.password}</span>
</>
)}
</p>
) : null}
@@ -12,11 +12,9 @@ import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
export interface CreateUserFormData {
employeeNo: string;
name: string;
phone: string;
email: string;
password: string;
departmentId: string | undefined;
directManagerId: string | null;
roles: string[];
@@ -48,16 +46,13 @@ export function CreateUserDialog({
managers,
roleOptions,
}: CreateUserDialogProps) {
const [employeeNo, setEmployeeNo] = useState("");
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [departmentId, setDepartmentId] = useState("");
const [directManagerId, setDirectManagerId] = useState("");
const [createRoles, setCreateRoles] = useState<Set<string>>(() => new Set(["employee"]));
const trimmedEmployeeNo = employeeNo.trim();
const trimmedName = name.trim();
const trimmedPhone = phone.trim();
const trimmedEmail = email.trim();
@@ -65,19 +60,15 @@ export function CreateUserDialog({
const canSubmit =
!isPending &&
trimmedEmployeeNo !== "" &&
trimmedName !== "" &&
trimmedPhone !== "" &&
trimmedEmail !== "" &&
password !== "" &&
rolesArray.length >= 1;
function resetForm() {
setEmployeeNo("");
setName("");
setPhone("");
setEmail("");
setPassword("");
setDepartmentId("");
setDirectManagerId("");
setCreateRoles(new Set(["employee"]));
@@ -92,11 +83,9 @@ export function CreateUserDialog({
function handleSubmit() {
onSubmit({
employeeNo: trimmedEmployeeNo,
name: trimmedName,
phone: trimmedPhone,
email: trimmedEmail,
password,
departmentId: departmentId || undefined,
directManagerId: directManagerId ? directManagerId : null,
roles: rolesArray,
@@ -113,20 +102,10 @@ export function CreateUserDialog({
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>新建用户</DialogTitle>
<DialogDescription>请填写基础信息与初始密码(满足密码策略)。</DialogDescription>
<DialogDescription>请填写基础信息。工号与一次性口令将在创建成功后由系统生成并展示一次。</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="employeeNo">工号</Label>
<Input
id="employeeNo"
data-testid="users-create-employeeNo"
value={employeeNo}
onChange={(e) => setEmployeeNo(e.target.value)}
placeholder="E1001"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="name">姓名</Label>
<Input
@@ -157,17 +136,6 @@ export function CreateUserDialog({
placeholder="[email protected]"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">初始密码</Label>
<Input
id="password"
data-testid="users-create-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="至少12位,含大小写与数字"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="departmentId">部门(可选)</Label>