This commit is contained in:
ditiqi 2025-01-26 12:45:59 +08:00
parent 2b57515c31
commit f966001505
16 changed files with 595 additions and 165 deletions

View File

@ -1,4 +1,19 @@
import { Controller, Headers, Post, Body, UseGuards, Get, Req, HttpException, HttpStatus, BadRequestException, InternalServerErrorException, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common';
import {
Controller,
Headers,
Post,
Body,
UseGuards,
Get,
Req,
HttpException,
HttpStatus,
BadRequestException,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthSchema, JwtPayload } from '@nice/common';
import { AuthGuard } from './auth.guard';
@ -7,8 +22,8 @@ import { z } from 'zod';
import { FileValidationErrorType } from './types';
@Controller('auth')
export class AuthController {
private logger = new Logger(AuthController.name)
constructor(private readonly authService: AuthService) { }
private logger = new Logger(AuthController.name);
constructor(private readonly authService: AuthService) {}
@Get('file')
async authFileRequset(
@Headers('x-original-uri') originalUri: string,
@ -18,7 +33,6 @@ export class AuthController {
@Headers('host') host: string,
@Headers('authorization') authorization: string,
) {
try {
const fileRequest = {
originalUri,
@ -26,10 +40,11 @@ export class AuthController {
method,
queryParams,
host,
authorization
authorization,
};
const authResult = await this.authService.validateFileRequest(fileRequest);
const authResult =
await this.authService.validateFileRequest(fileRequest);
if (!authResult.isValid) {
// 使用枚举类型进行错误处理
switch (authResult.error) {
@ -41,7 +56,9 @@ export class AuthController {
case FileValidationErrorType.INVALID_TOKEN:
throw new UnauthorizedException(authResult.error);
default:
throw new InternalServerErrorException(authResult.error || FileValidationErrorType.UNKNOWN_ERROR);
throw new InternalServerErrorException(
authResult.error || FileValidationErrorType.UNKNOWN_ERROR,
);
}
}
return {
@ -51,17 +68,20 @@ export class AuthController {
},
};
} catch (error: any) {
this.logger.verbose(`File request auth failed from ${realIp} reason:${error.message}`)
this.logger.verbose(
`File request auth failed from ${realIp} reason:${error.message}`,
);
throw error;
}
}
@UseGuards(AuthGuard)
@Get('user-profile')
async getUserProfile(@Req() request: Request) {
const payload: JwtPayload = (request as any).user;
const { staff } = await UserProfileService.instance.getUserProfileById(payload.sub);
return staff
const { staff } = await UserProfileService.instance.getUserProfileById(
payload.sub,
);
return staff;
}
@Post('login')
async login(@Body() body: z.infer<typeof AuthSchema.signInRequset>) {

View File

@ -146,6 +146,9 @@ export class AuthService {
showname,
photoUrl,
deptId,
office,
email,
rank,
...others
} = data;
const existingUser = await db.staff.findFirst({
@ -175,6 +178,9 @@ export class AuthService {
// domainId: data.deptId,
meta: {
photoUrl,
office,
rank,
email,
},
},
});

View File

@ -2,14 +2,21 @@ import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
import DepartmentSelect from "@web/src/components/models/department/department-select";
import { Form, Input, Button, Select } from "antd";
import { motion } from "framer-motion";
import { useState } from "react";
export interface RegisterFormData {
deptId: string;
domainId: string;
username: string;
showname: string;
officerId: string;
password: string;
repeatPass: string;
rank: string;
office: string;
email: string;
phoneNumber: string;
}
interface RegisterFormProps {
@ -19,7 +26,7 @@ interface RegisterFormProps {
export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
const [form] = Form.useForm<RegisterFormData>();
const [domainId, setDomainId] = useState<string>();
return (
<motion.div
initial={{ opacity: 0 }}
@ -34,10 +41,11 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
<div>
<Form.Item name="photoUrl" label="头像" noStyle>
<AvatarUploader
className="rounded-lg"
placeholder="点击上传头像"
style={{
height: 120,
width: 100,
height: 150,
width: 120,
}}></AvatarUploader>
</Form.Item>
</div>
@ -62,67 +70,126 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
]}>
<Input placeholder="姓名" />
</Form.Item>
<Form.Item
noStyle
name={"domainId"}
label="所属域"
rules={[{ required: true }]}>
<DepartmentSelect
placeholder="选择域"
onChange={(value) => {
setDomainId(value as string);
}}
domain={true}
/>
</Form.Item>
<Form.Item
name="deptId"
noStyle
label="部门"
rules={[{ required: true, message: "请选择部门" }]}>
<DepartmentSelect></DepartmentSelect>
<DepartmentSelect
rootId={domainId}></DepartmentSelect>
</Form.Item>
</div>
</div>
<Form.Item
name="officerId"
label="证件号"
rules={[
{ required: true, message: "请输入证件号" },
{
pattern: /^\d{5,12}$/,
message: "请输入有效的证件号5-12位数字",
},
]}>
<Input placeholder="证件号" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[
{ required: true, message: "请输入密码" },
{ min: 8, message: "密码至少需要8个字符" },
{
pattern:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
message: "密码必须包含大小写字母、数字和特殊字符",
},
]}>
<Input.Password placeholder="密码" />
</Form.Item>
<Form.Item
name="repeatPass"
label="确认密码"
dependencies={["password"]}
rules={[
{ required: true, message: "请确认密码" },
({ getFieldValue }) => ({
validator(_, value) {
if (
!value ||
getFieldValue("password") === value
) {
return Promise.resolve();
}
return Promise.reject(
new Error("两次输入的密码不一致")
);
<div className="grid grid-cols-1 gap-2 flex-1 mb-2">
<Form.Item noStyle name={"rank"}>
<Input
placeholder="请输入职级(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item
name="officerId"
label="证件号"
noStyle
rules={[
{ required: true, message: "请输入证件号" },
{
pattern: /^\d{5,12}$/,
message: "请输入有效的证件号5-12位数字",
},
}),
]}>
<Input.Password placeholder="确认密码" />
</Form.Item>
]}>
<Input placeholder="证件号(可选)" />
</Form.Item>
<Form.Item
noStyle
rules={[
{
required: false,
pattern: /^\d{6,11}$/,
message: "请输入正确的手机号(数字)",
},
]}
name={"phoneNumber"}
label="手机号">
<Input
autoComplete="new-phone" // 使用非标准的自动完成值
spellCheck={false}
allowClear
placeholder="请输入手机号(可选)"
/>
</Form.Item>
<Form.Item noStyle name={"email"}>
<Input
placeholder="请输入邮箱(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item noStyle name={"office"}>
<Input
placeholder="请输入办公室地点(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item
name="password"
label="密码"
noStyle
rules={[
{ required: true, message: "请输入密码" },
{ min: 8, message: "密码至少需要8个字符" },
{
pattern:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
message:
"密码必须包含大小写字母、数字和特殊字符",
},
]}>
<Input.Password placeholder="密码" />
</Form.Item>
<Form.Item
name="repeatPass"
label="确认密码"
noStyle
dependencies={["password"]}
rules={[
{ required: true, message: "请确认密码" },
({ getFieldValue }) => ({
validator(_, value) {
if (
!value ||
getFieldValue("password") === value
) {
return Promise.resolve();
}
return Promise.reject(
new Error("两次输入的密码不一致")
);
},
}),
]}>
<Input.Password placeholder="确认密码" />
</Form.Item>
</div>
<Form.Item>
<Button
type="primary"

View File

@ -39,7 +39,7 @@ export default function WriteLetterPage() {
],
},
orderBy: {
order: "desc",
order: "asc",
},
// orderBy:{

View File

@ -143,7 +143,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
<p className="ant-upload-hint"></p>
{/* 正在上传的文件 */}
{(uploadingFiles.length > 0 || completedFiles.length > 0) && (
<div className=" p-2 border rounded bg-white mt-1">
<div className=" px-2 py-0 border rounded bg-white mt-1 ">
{uploadingFiles.map((file) => (
<div
key={file.fileKey}
@ -177,7 +177,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
completedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between gap-2 py-2">
className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<CheckCircleOutlined className="text-green-500" />
<div className="text-sm">
@ -188,10 +188,12 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
type="text"
danger
icon={<DeleteOutlined />}
onClick={() =>
file.fileId &&
handleRemoveFile(file.fileId)
}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
if (file.fileId) {
handleRemoveFile(file.fileId); // 只删除文件
}
}}
/>
</div>
))}

View File

@ -0,0 +1,26 @@
import { Button, Drawer, Modal } from "antd";
import React, { useContext, useEffect, useState } from "react";
import { UserEditorContext } from "./usermenu";
import UserForm from "./user-form";
export default function UserEditModal() {
const { formLoading, modalOpen, setModalOpen, form } =
useContext(UserEditorContext);
const handleOk = () => {
form.submit();
};
return (
<Modal
width={400}
onOk={handleOk}
open={modalOpen}
confirmLoading={formLoading}
onCancel={() => {
setModalOpen(false);
}}
title={"编辑个人信息"}>
<UserForm />
</Modal>
);
}

View File

@ -0,0 +1,248 @@
import { Button, Form, Input, Spin, Switch, message } from "antd";
import { useContext, useEffect } from "react";
import { useStaff } from "@nice/client";
import DepartmentSelect from "@web/src/components/models/department/department-select";
import { api } from "@nice/client";
import { useAuth } from "@web/src/providers/auth-provider";
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
import { StaffDto } from "@nice/common";
import { UserEditorContext } from "./usermenu";
import toast from "react-hot-toast";
export default function StaffForm() {
const { user } = useAuth();
const { create, update } = useStaff(); // Ensure you have these methods in your hooks
const {
formLoading,
modalOpen,
setModalOpen,
domainId,
setDomainId,
form,
setFormLoading,
} = useContext(UserEditorContext);
const {
data,
isLoading,
}: {
data: StaffDto;
isLoading: boolean;
} = api.staff.findFirst.useQuery(
{ where: { id: user?.id } },
{ enabled: !!user?.id }
);
const { isRoot } = useAuth();
async function handleFinish(values: any) {
const {
username,
showname,
deptId,
domainId,
password,
phoneNumber,
officerId,
enabled,
photoUrl,
email,
rank,
office,
} = values;
setFormLoading(true);
try {
if (data && user?.id) {
await update.mutateAsync({
where: { id: data.id },
data: {
username,
deptId,
showname,
domainId,
password,
phoneNumber,
officerId,
enabled,
meta: {
photoUrl,
email,
rank,
office,
},
},
});
}
toast.success("提交成功");
setModalOpen(false);
} catch (err: any) {
toast.error(err.message);
} finally {
setFormLoading(false);
}
}
useEffect(() => {
form.resetFields();
if (data) {
form.setFieldValue("username", data.username);
form.setFieldValue("showname", data.showname);
form.setFieldValue("domainId", data.domainId);
form.setFieldValue("deptId", data.deptId);
form.setFieldValue("officerId", data.officerId);
form.setFieldValue("phoneNumber", data.phoneNumber);
form.setFieldValue("enabled", data.enabled);
form.setFieldValue("photoUrl", data?.meta?.photoUrl);
form.setFieldValue("email", data?.meta?.email);
form.setFieldValue("rank", data?.meta?.rank);
form.setFieldValue("office", data?.meta?.office);
}
}, [data]);
// useEffect(() => {
// if (!data && domainId) {
// form.setFieldValue("domainId", domainId);
// form.setFieldValue("deptId", domainId);
// }
// }, [domainId, data as any]);
return (
<div className="relative">
{isLoading && (
<div className="absolute h-full inset-0 flex items-center justify-center bg-white bg-opacity-50 z-10">
<Spin />
</div>
)}
<Form
disabled={isLoading}
form={form}
layout="vertical"
requiredMark="optional"
autoComplete="off"
onFinish={handleFinish}>
<div className=" flex items-center gap-4 mb-2">
<div>
<Form.Item name={"photoUrl"} label="头像" noStyle>
<AvatarUploader
placeholder="点击上传头像"
className="rounded-lg"
style={{
width: "120px",
height: "150px",
}}></AvatarUploader>
</Form.Item>
</div>
<div className="grid grid-cols-1 gap-2 flex-1">
<Form.Item
noStyle
rules={[{ required: true }]}
name={"username"}
label="帐号">
<Input
placeholder="请输入用户名"
allowClear
autoComplete="new-username" // 使用非标准的自动完成值
spellCheck={false}
/>
</Form.Item>
<Form.Item
noStyle
rules={[{ required: true }]}
name={"showname"}
label="姓名">
<Input
placeholder="请输入姓名"
allowClear
autoComplete="new-name" // 使用非标准的自动完成值
spellCheck={false}
/>
</Form.Item>
<Form.Item
name={"domainId"}
label="所属域"
noStyle
rules={[{ required: true }]}>
<DepartmentSelect
placeholder="选择域"
onChange={(value) => {
setDomainId(value as string);
}}
domain={true}
/>
</Form.Item>
<Form.Item
noStyle
name={"deptId"}
label="所属单位"
rules={[{ required: true }]}>
<DepartmentSelect rootId={domainId} />
</Form.Item>
</div>
</div>
<div className="grid grid-cols-1 gap-2 flex-1">
<Form.Item noStyle name={"rank"}>
<Input
placeholder="请输入职级(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item
rules={[
{
required: false,
pattern: /^\d{5,18}$/,
message: "请输入正确的证件号(数字)",
},
]}
noStyle
name={"officerId"}>
<Input
placeholder="请输入证件号(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item
rules={[
{
required: false,
pattern: /^\d{6,11}$/,
message: "请输入正确的手机号(数字)",
},
]}
noStyle
name={"phoneNumber"}
label="手机号">
<Input
placeholder="请输入手机号(可选)"
autoComplete="new-phone" // 使用非标准的自动完成值
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item noStyle name={"email"}>
<Input
placeholder="请输入邮箱(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item noStyle name={"office"}>
<Input
placeholder="请输入办公室地点(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item noStyle label="密码" name={"password"}>
<Input.Password
placeholder="修改密码"
spellCheck={false}
visibilityToggle
autoComplete="new-password"
/>
</Form.Item>
</div>
</Form>
</div>
);
}

View File

@ -1,18 +1,25 @@
import { useClickOutside } from "@web/src/hooks/useClickOutside";
import { useAuth } from "@web/src/providers/auth-provider";
import { motion, AnimatePresence } from "framer-motion";
import React,{ useState, useRef, useCallback, useMemo, createContext } from "react";
import { Avatar } from "../../common/element/Avatar";
import React, {
useState,
useRef,
useCallback,
useMemo,
createContext,
} from "react";
import { Avatar } from "../../../common/element/Avatar";
import {
UserOutlined,
SettingOutlined,
QuestionCircleOutlined,
LogoutOutlined,
} from "@ant-design/icons";
import { FormInstance, Spin } from "antd";
import { useNavigate } from "react-router-dom";
import { MenuItemType } from "./types";
import { MenuItemType } from "../types";
import { RolePerms } from "@nice/common";
import { useForm } from "antd/es/form/Form";
import UserEditModal from "./user-edit-modal";
const menuVariants = {
hidden: { opacity: 0, scale: 0.95, y: -10 },
visible: {
@ -36,36 +43,33 @@ const menuVariants = {
};
export const UserEditorContext = createContext<{
domainId: string;
modalOpen: boolean;
setDomainId: React.Dispatch<React.SetStateAction<string>>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
editId: string;
setEditId: React.Dispatch<React.SetStateAction<string>>;
form: FormInstance<any>;
formLoading: boolean;
setFormLoading: React.Dispatch<React.SetStateAction<boolean>>;
canManageAnyStaff: boolean;
domainId: string;
setDomainId: React.Dispatch<React.SetStateAction<string>>;
modalOpen: boolean;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
form: FormInstance<any>;
formLoading: boolean;
setFormLoading: React.Dispatch<React.SetStateAction<boolean>>;
}>({
domainId: undefined,
modalOpen: false,
setDomainId: undefined,
setModalOpen: undefined,
editId: undefined,
setEditId: undefined,
form: undefined,
formLoading: undefined,
setFormLoading: undefined,
canManageAnyStaff: false,
modalOpen: false,
domainId: undefined,
setDomainId: undefined,
setModalOpen: undefined,
form: undefined,
formLoading: undefined,
setFormLoading: undefined,
});
export function UserMenu() {
const [form] = useForm();
const [formLoading, setFormLoading] = useState<boolean>();
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const { user, logout, isLoading, hasSomePermissions } = useAuth();
const navigate = useNavigate();
useClickOutside(menuRef, () => setShowMenu(false));
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [domainId, setDomainId] = useState<string>();
const toggleMenu = useCallback(() => {
setShowMenu((prev) => !prev);
}, []);
@ -78,7 +82,9 @@ export function UserMenu() {
{
icon: <UserOutlined className="text-lg" />,
label: "个人信息",
action: () => {},
action: () => {
setModalOpen(true);
},
},
canManageAnyStaff && {
icon: <SettingOutlined className="text-lg" />,
@ -115,7 +121,16 @@ export function UserMenu() {
}
return (
<UserEditorContext.Provider
value={{
formLoading,
setFormLoading,
form,
domainId,
modalOpen,
setDomainId,
setModalOpen,
}}>
<div ref={menuRef} className="relative">
<motion.button
aria-label="用户菜单"
@ -238,6 +253,7 @@ export function UserMenu() {
)}
</AnimatePresence>
</div>
<UserEditModal></UserEditModal>
</UserEditorContext.Provider>
);
}

View File

@ -4,7 +4,7 @@ import { SearchBar } from "./SearchBar";
import Navigation from "./navigation";
import { useAuth } from "@web/src/providers/auth-provider";
import { UserOutlined } from "@ant-design/icons";
import { UserMenu } from "../element/usermenu";
import { UserMenu } from "../element/usermenu/usermenu";
import SineWavesCanvas from "../../animation/sine-wave";
interface HeaderProps {
onSearch?: (query: string) => void;

View File

@ -81,7 +81,7 @@ export function useNavItem() {
...categoryItems,
// staticItems.help,
].filter(Boolean);
}, [data]);
}, [data, user]);
return { navItems };
}

View File

@ -46,6 +46,7 @@ export function LetterBasicForm() {
<Select
mode="tags"
showSearch={false}
suffixIcon={null}
placeholder="输入标签后按回车添加"
value={form.getFieldValue(["meta", "tags"]) || []}
onChange={(value) =>
@ -54,16 +55,6 @@ export function LetterBasicForm() {
tokenSeparators={[",", " "]}
className="w-full"
dropdownStyle={{ display: "none" }}
tagRender={({ label, onClose }) => (
<div className="bg-primary-50 text-primary-600 px-3 py-1 rounded-full text-sm mr-2 mb-1 flex items-center">
{label}
<span
className="ml-2 cursor-pointer hover:text-primary-700"
onClick={onClose}>
×
</span>
</div>
)}
/>
</Form.Item>

View File

@ -34,11 +34,11 @@ export const StaffEditorContext = createContext<{
});
export default function StaffEditor() {
const [form] = useForm();
const [formLoading, setFormLoading] = useState<boolean>();
const [domainId, setDomainId] = useState<string>();
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editId, setEditId] = useState<string>();
const { user, hasSomePermissions } = useAuth();
const [formLoading, setFormLoading] = useState<boolean>();
useEffect(() => {
if (user) {
setDomainId(user.domainId);

View File

@ -41,6 +41,9 @@ export default function StaffForm() {
officerId,
enabled,
photoUrl,
email,
rank,
office,
} = values;
setFormLoading(true);
try {
@ -58,6 +61,9 @@ export default function StaffForm() {
enabled,
meta: {
photoUrl,
email,
rank,
office,
},
},
});
@ -73,6 +79,9 @@ export default function StaffForm() {
phoneNumber,
meta: {
photoUrl,
email,
rank,
office,
},
},
});
@ -100,6 +109,9 @@ export default function StaffForm() {
form.setFieldValue("phoneNumber", data.phoneNumber);
form.setFieldValue("enabled", data.enabled);
form.setFieldValue("photoUrl", data?.meta?.photoUrl);
form.setFieldValue("email", data?.meta?.email);
form.setFieldValue("rank", data?.meta?.rank);
form.setFieldValue("office", data?.meta?.office);
}
}, [data]);
useEffect(() => {
@ -126,9 +138,11 @@ export default function StaffForm() {
<div>
<Form.Item name={"photoUrl"} label="头像" noStyle>
<AvatarUploader
placeholder="点击上传头像"
className="rounded-lg"
style={{
width: "100px",
height: "120px",
width: "120px",
height: "150px",
}}></AvatarUploader>
</Form.Item>
</div>
@ -157,6 +171,17 @@ export default function StaffForm() {
spellCheck={false}
/>
</Form.Item>
<Form.Item
noStyle
name={"domainId"}
label="所属域"
rules={[{ required: true }]}>
<DepartmentSelect
placeholder="选择域"
rootId={isRoot ? undefined : domainId}
domain={true}
/>
</Form.Item>
<Form.Item
noStyle
name={"deptId"}
@ -168,58 +193,81 @@ export default function StaffForm() {
</Form.Item>
</div>
</div>
{canManageAnyStaff && (
<Form.Item
name={"domainId"}
label="所属域"
rules={[{ required: true }]}>
<DepartmentSelect
placeholder="选择域"
rootId={isRoot ? undefined : domainId}
domain={true}
<div className="grid grid-cols-1 gap-2 flex-1">
<Form.Item noStyle name={"rank"}>
<Input
placeholder="请输入职级(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
)}
<Form.Item
rules={[
{
required: false,
pattern: /^\d{5,18}$/,
message: "请输入正确的证件号(数字)",
},
]}
name={"officerId"}
label="证件号">
<Input autoComplete="off" spellCheck={false} allowClear />
</Form.Item>
<Form.Item
rules={[
{
required: false,
pattern: /^\d{6,11}$/,
message: "请输入正确的手机号(数字)",
},
]}
name={"phoneNumber"}
label="手机号">
<Input
autoComplete="new-phone" // 使用非标准的自动完成值
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item label="密码" name={"password"}>
<Input.Password
spellCheck={false}
visibilityToggle
autoComplete="new-password"
/>
</Form.Item>
{editId && (
<Form.Item label="是否启用" name={"enabled"}>
<Switch></Switch>
<Form.Item
noStyle
rules={[
{
required: false,
pattern: /^\d{5,18}$/,
message: "请输入正确的证件号(数字)",
},
]}
name={"officerId"}
label="证件号">
<Input
autoComplete="off"
spellCheck={false}
allowClear
placeholder="请输入证件号(可选)"
/>
</Form.Item>
)}
<Form.Item
noStyle
rules={[
{
required: false,
pattern: /^\d{6,11}$/,
message: "请输入正确的手机号(数字)",
},
]}
name={"phoneNumber"}
label="手机号">
<Input
autoComplete="new-phone" // 使用非标准的自动完成值
spellCheck={false}
allowClear
placeholder="请输入手机号(可选)"
/>
</Form.Item>
<Form.Item noStyle name={"email"}>
<Input
placeholder="请输入邮箱(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item noStyle name={"office"}>
<Input
placeholder="请输入办公室地点(可选)"
autoComplete="off"
spellCheck={false}
allowClear
/>
</Form.Item>
<Form.Item label="密码" name={"password"} noStyle>
<Input.Password
spellCheck={false}
visibilityToggle
autoComplete="new-password"
placeholder="请输入密码"
/>
</Form.Item>
{editId && (
<Form.Item label="是否启用" name={"enabled"}>
<Switch></Switch>
</Form.Item>
)}
</div>
</Form>
</div>
);

View File

@ -11,7 +11,7 @@ interface StaffSelectProps {
multiple?: boolean;
domainId?: string;
placeholder?: string;
size?: SizeType
size?: SizeType;
}
export default function StaffSelect({
@ -21,7 +21,7 @@ export default function StaffSelect({
style,
multiple,
domainId,
size
size,
}: StaffSelectProps) {
const [keyword, setQuery] = useState<string>("");

View File

@ -75,8 +75,10 @@ model Staff {
username String @unique @map("username")
avatar String? @map("avatar")
password String? @map("password")
phoneNumber String? @unique @map("phone_number")
domainId String? @map("domain_id")
deptId String? @map("dept_id")

View File

@ -5,10 +5,14 @@ export const AuthSchema = {
username: z.string(),
password: z.string(),
deptId: z.string().nullish(),
domainId: z.string().nullish(),
officerId: z.string().nullish(),
showname: z.string().nullish(),
phoneNumber: z.string().nullish(),
photoUrl: z.string().nullish(),
rank: z.string().nullish(),
office: z.string().nullish(),
email: z.string().nullish(),
}),
refreshTokenRequest: z.object({
refreshToken: z.string(),