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

View File

@ -146,6 +146,9 @@ export class AuthService {
showname, showname,
photoUrl, photoUrl,
deptId, deptId,
office,
email,
rank,
...others ...others
} = data; } = data;
const existingUser = await db.staff.findFirst({ const existingUser = await db.staff.findFirst({
@ -175,6 +178,9 @@ export class AuthService {
// domainId: data.deptId, // domainId: data.deptId,
meta: { meta: {
photoUrl, 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 DepartmentSelect from "@web/src/components/models/department/department-select";
import { Form, Input, Button, Select } from "antd"; import { Form, Input, Button, Select } from "antd";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useState } from "react";
export interface RegisterFormData { export interface RegisterFormData {
deptId: string; deptId: string;
domainId: string;
username: string; username: string;
showname: string; showname: string;
officerId: string; officerId: string;
password: string; password: string;
repeatPass: string; repeatPass: string;
rank: string;
office: string;
email: string;
phoneNumber: string;
} }
interface RegisterFormProps { interface RegisterFormProps {
@ -19,7 +26,7 @@ interface RegisterFormProps {
export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => { export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
const [form] = Form.useForm<RegisterFormData>(); const [form] = Form.useForm<RegisterFormData>();
const [domainId, setDomainId] = useState<string>();
return ( return (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@ -34,10 +41,11 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
<div> <div>
<Form.Item name="photoUrl" label="头像" noStyle> <Form.Item name="photoUrl" label="头像" noStyle>
<AvatarUploader <AvatarUploader
className="rounded-lg"
placeholder="点击上传头像" placeholder="点击上传头像"
style={{ style={{
height: 120, height: 150,
width: 100, width: 120,
}}></AvatarUploader> }}></AvatarUploader>
</Form.Item> </Form.Item>
</div> </div>
@ -62,19 +70,42 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
]}> ]}>
<Input placeholder="姓名" /> <Input placeholder="姓名" />
</Form.Item> </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 <Form.Item
name="deptId" name="deptId"
noStyle noStyle
label="部门" label="部门"
rules={[{ required: true, message: "请选择部门" }]}> rules={[{ required: true, message: "请选择部门" }]}>
<DepartmentSelect></DepartmentSelect> <DepartmentSelect
rootId={domainId}></DepartmentSelect>
</Form.Item> </Form.Item>
</div> </div>
</div> </div>
<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 <Form.Item
name="officerId" name="officerId"
label="证件号" label="证件号"
noStyle
rules={[ rules={[
{ required: true, message: "请输入证件号" }, { required: true, message: "请输入证件号" },
{ {
@ -82,19 +113,54 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
message: "请输入有效的证件号5-12位数字", message: "请输入有效的证件号5-12位数字",
}, },
]}> ]}>
<Input placeholder="证件号" /> <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>
<Form.Item <Form.Item
name="password" name="password"
label="密码" label="密码"
noStyle
rules={[ rules={[
{ required: true, message: "请输入密码" }, { required: true, message: "请输入密码" },
{ min: 8, message: "密码至少需要8个字符" }, { min: 8, message: "密码至少需要8个字符" },
{ {
pattern: pattern:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
message: "密码必须包含大小写字母、数字和特殊字符", message:
"密码必须包含大小写字母、数字和特殊字符",
}, },
]}> ]}>
<Input.Password placeholder="密码" /> <Input.Password placeholder="密码" />
@ -103,6 +169,7 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
<Form.Item <Form.Item
name="repeatPass" name="repeatPass"
label="确认密码" label="确认密码"
noStyle
dependencies={["password"]} dependencies={["password"]}
rules={[ rules={[
{ required: true, message: "请确认密码" }, { required: true, message: "请确认密码" },
@ -122,7 +189,7 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
]}> ]}>
<Input.Password placeholder="确认密码" /> <Input.Password placeholder="确认密码" />
</Form.Item> </Form.Item>
</div>
<Form.Item> <Form.Item>
<Button <Button
type="primary" type="primary"

View File

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

View File

@ -143,7 +143,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
<p className="ant-upload-hint"></p> <p className="ant-upload-hint"></p>
{/* 正在上传的文件 */} {/* 正在上传的文件 */}
{(uploadingFiles.length > 0 || completedFiles.length > 0) && ( {(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) => ( {uploadingFiles.map((file) => (
<div <div
key={file.fileKey} key={file.fileKey}
@ -177,7 +177,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
completedFiles.map((file, index) => ( completedFiles.map((file, index) => (
<div <div
key={index} 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"> <div className="flex items-center gap-2">
<CheckCircleOutlined className="text-green-500" /> <CheckCircleOutlined className="text-green-500" />
<div className="text-sm"> <div className="text-sm">
@ -188,10 +188,12 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
type="text" type="text"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={() => onClick={(e) => {
file.fileId && e.stopPropagation(); // 阻止事件冒泡
handleRemoveFile(file.fileId) if (file.fileId) {
handleRemoveFile(file.fileId); // 只删除文件
} }
}}
/> />
</div> </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 { useClickOutside } from "@web/src/hooks/useClickOutside";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import React,{ useState, useRef, useCallback, useMemo, createContext } from "react"; import React, {
import { Avatar } from "../../common/element/Avatar"; useState,
useRef,
useCallback,
useMemo,
createContext,
} from "react";
import { Avatar } from "../../../common/element/Avatar";
import { import {
UserOutlined, UserOutlined,
SettingOutlined, SettingOutlined,
QuestionCircleOutlined,
LogoutOutlined, LogoutOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { FormInstance, Spin } from "antd"; import { FormInstance, Spin } from "antd";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { MenuItemType } from "./types"; import { MenuItemType } from "../types";
import { RolePerms } from "@nice/common"; import { RolePerms } from "@nice/common";
import { useForm } from "antd/es/form/Form";
import UserEditModal from "./user-edit-modal";
const menuVariants = { const menuVariants = {
hidden: { opacity: 0, scale: 0.95, y: -10 }, hidden: { opacity: 0, scale: 0.95, y: -10 },
visible: { visible: {
@ -37,35 +44,32 @@ const menuVariants = {
export const UserEditorContext = createContext<{ export const UserEditorContext = createContext<{
domainId: string; domainId: string;
modalOpen: boolean;
setDomainId: React.Dispatch<React.SetStateAction<string>>; setDomainId: React.Dispatch<React.SetStateAction<string>>;
modalOpen: boolean;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>; setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
editId: string;
setEditId: React.Dispatch<React.SetStateAction<string>>;
form: FormInstance<any>; form: FormInstance<any>;
formLoading: boolean; formLoading: boolean;
setFormLoading: React.Dispatch<React.SetStateAction<boolean>>; setFormLoading: React.Dispatch<React.SetStateAction<boolean>>;
canManageAnyStaff: boolean;
}>({ }>({
domainId: undefined,
modalOpen: false, modalOpen: false,
domainId: undefined,
setDomainId: undefined, setDomainId: undefined,
setModalOpen: undefined, setModalOpen: undefined,
editId: undefined,
setEditId: undefined,
form: undefined, form: undefined,
formLoading: undefined, formLoading: undefined,
setFormLoading: undefined, setFormLoading: undefined,
canManageAnyStaff: false,
}); });
export function UserMenu() { export function UserMenu() {
const [form] = useForm();
const [formLoading, setFormLoading] = useState<boolean>();
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const { user, logout, isLoading, hasSomePermissions } = useAuth(); const { user, logout, isLoading, hasSomePermissions } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
useClickOutside(menuRef, () => setShowMenu(false)); useClickOutside(menuRef, () => setShowMenu(false));
const [modalOpen, setModalOpen] = useState<boolean>(false); const [modalOpen, setModalOpen] = useState<boolean>(false);
const [domainId, setDomainId] = useState<string>();
const toggleMenu = useCallback(() => { const toggleMenu = useCallback(() => {
setShowMenu((prev) => !prev); setShowMenu((prev) => !prev);
}, []); }, []);
@ -78,7 +82,9 @@ export function UserMenu() {
{ {
icon: <UserOutlined className="text-lg" />, icon: <UserOutlined className="text-lg" />,
label: "个人信息", label: "个人信息",
action: () => {}, action: () => {
setModalOpen(true);
},
}, },
canManageAnyStaff && { canManageAnyStaff && {
icon: <SettingOutlined className="text-lg" />, icon: <SettingOutlined className="text-lg" />,
@ -115,7 +121,16 @@ export function UserMenu() {
} }
return ( return (
<UserEditorContext.Provider
value={{
formLoading,
setFormLoading,
form,
domainId,
modalOpen,
setDomainId,
setModalOpen,
}}>
<div ref={menuRef} className="relative"> <div ref={menuRef} className="relative">
<motion.button <motion.button
aria-label="用户菜单" aria-label="用户菜单"
@ -238,6 +253,7 @@ export function UserMenu() {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
<UserEditModal></UserEditModal>
</UserEditorContext.Provider>
); );
} }

View File

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

View File

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

View File

@ -46,6 +46,7 @@ export function LetterBasicForm() {
<Select <Select
mode="tags" mode="tags"
showSearch={false} showSearch={false}
suffixIcon={null}
placeholder="输入标签后按回车添加" placeholder="输入标签后按回车添加"
value={form.getFieldValue(["meta", "tags"]) || []} value={form.getFieldValue(["meta", "tags"]) || []}
onChange={(value) => onChange={(value) =>
@ -54,16 +55,6 @@ export function LetterBasicForm() {
tokenSeparators={[",", " "]} tokenSeparators={[",", " "]}
className="w-full" className="w-full"
dropdownStyle={{ display: "none" }} 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> </Form.Item>

View File

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

View File

@ -41,6 +41,9 @@ export default function StaffForm() {
officerId, officerId,
enabled, enabled,
photoUrl, photoUrl,
email,
rank,
office,
} = values; } = values;
setFormLoading(true); setFormLoading(true);
try { try {
@ -58,6 +61,9 @@ export default function StaffForm() {
enabled, enabled,
meta: { meta: {
photoUrl, photoUrl,
email,
rank,
office,
}, },
}, },
}); });
@ -73,6 +79,9 @@ export default function StaffForm() {
phoneNumber, phoneNumber,
meta: { meta: {
photoUrl, photoUrl,
email,
rank,
office,
}, },
}, },
}); });
@ -100,6 +109,9 @@ export default function StaffForm() {
form.setFieldValue("phoneNumber", data.phoneNumber); form.setFieldValue("phoneNumber", data.phoneNumber);
form.setFieldValue("enabled", data.enabled); form.setFieldValue("enabled", data.enabled);
form.setFieldValue("photoUrl", data?.meta?.photoUrl); 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]); }, [data]);
useEffect(() => { useEffect(() => {
@ -126,9 +138,11 @@ export default function StaffForm() {
<div> <div>
<Form.Item name={"photoUrl"} label="头像" noStyle> <Form.Item name={"photoUrl"} label="头像" noStyle>
<AvatarUploader <AvatarUploader
placeholder="点击上传头像"
className="rounded-lg"
style={{ style={{
width: "100px", width: "120px",
height: "120px", height: "150px",
}}></AvatarUploader> }}></AvatarUploader>
</Form.Item> </Form.Item>
</div> </div>
@ -159,17 +173,6 @@ export default function StaffForm() {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
noStyle noStyle
name={"deptId"}
label="所属单位"
rules={[{ required: true }]}>
<DepartmentSelect
rootId={isRoot ? undefined : domainId}
/>
</Form.Item>
</div>
</div>
{canManageAnyStaff && (
<Form.Item
name={"domainId"} name={"domainId"}
label="所属域" label="所属域"
rules={[{ required: true }]}> rules={[{ required: true }]}>
@ -179,8 +182,28 @@ export default function StaffForm() {
domain={true} domain={true}
/> />
</Form.Item> </Form.Item>
)}
<Form.Item <Form.Item
noStyle
name={"deptId"}
label="所属单位"
rules={[{ required: true }]}>
<DepartmentSelect
rootId={isRoot ? undefined : 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
noStyle
rules={[ rules={[
{ {
required: false, required: false,
@ -190,9 +213,15 @@ export default function StaffForm() {
]} ]}
name={"officerId"} name={"officerId"}
label="证件号"> label="证件号">
<Input autoComplete="off" spellCheck={false} allowClear /> <Input
autoComplete="off"
spellCheck={false}
allowClear
placeholder="请输入证件号(可选)"
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
noStyle
rules={[ rules={[
{ {
required: false, required: false,
@ -206,13 +235,31 @@ export default function StaffForm() {
autoComplete="new-phone" // 使用非标准的自动完成值 autoComplete="new-phone" // 使用非标准的自动完成值
spellCheck={false} spellCheck={false}
allowClear allowClear
placeholder="请输入手机号(可选)"
/> />
</Form.Item> </Form.Item>
<Form.Item label="密码" name={"password"}> <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 <Input.Password
spellCheck={false} spellCheck={false}
visibilityToggle visibilityToggle
autoComplete="new-password" autoComplete="new-password"
placeholder="请输入密码"
/> />
</Form.Item> </Form.Item>
{editId && ( {editId && (
@ -220,6 +267,7 @@ export default function StaffForm() {
<Switch></Switch> <Switch></Switch>
</Form.Item> </Form.Item>
)} )}
</div>
</Form> </Form>
</div> </div>
); );

View File

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

View File

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

View File

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