add
This commit is contained in:
parent
2b57515c31
commit
f966001505
|
@ -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>) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -39,7 +39,7 @@ export default function WriteLetterPage() {
|
|||
],
|
||||
},
|
||||
orderBy: {
|
||||
order: "desc",
|
||||
order: "asc",
|
||||
},
|
||||
// orderBy:{
|
||||
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -81,7 +81,7 @@ export function useNavItem() {
|
|||
...categoryItems,
|
||||
// staticItems.help,
|
||||
].filter(Boolean);
|
||||
}, [data]);
|
||||
}, [data, user]);
|
||||
|
||||
return { navItems };
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>("");
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in New Issue