add
This commit is contained in:
parent
ac18602e58
commit
f121322bbd
|
@ -0,0 +1,131 @@
|
||||||
|
import { env } from "@web/src/env";
|
||||||
|
import { message, Progress, Spin, theme } from "antd";
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||||
|
|
||||||
|
export interface AvatarUploaderProps {
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
style?: React.CSSProperties; // 添加style属性
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadingFile {
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
status: "uploading" | "done" | "error";
|
||||||
|
fileId?: string;
|
||||||
|
fileKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
placeholder = "点击上传",
|
||||||
|
style, // 解构style属性
|
||||||
|
}) => {
|
||||||
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||||||
|
const [file, setFile] = useState<UploadingFile | null>(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedFile = event.target.files?.[0];
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
setFile({
|
||||||
|
name: selectedFile.name,
|
||||||
|
progress: 0,
|
||||||
|
status: "uploading",
|
||||||
|
fileKey: `${selectedFile.name}-${Date.now()}`,
|
||||||
|
});
|
||||||
|
setUploading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileId = await new Promise<string>((resolve, reject) => {
|
||||||
|
handleFileUpload(
|
||||||
|
selectedFile,
|
||||||
|
(result) => {
|
||||||
|
setFile((prev) => ({
|
||||||
|
...prev!,
|
||||||
|
progress: 100,
|
||||||
|
status: "done",
|
||||||
|
fileId: result.fileId,
|
||||||
|
}));
|
||||||
|
resolve(result.fileId);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
file?.fileKey
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setPreviewUrl(`${env.SERVER_IP}/uploads/${fileId}`);
|
||||||
|
onChange?.(fileId);
|
||||||
|
message.success("头像上传成功");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("上传错误:", error);
|
||||||
|
message.error("头像上传失败");
|
||||||
|
setFile((prev) => ({ ...prev!, status: "error" }));
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerUpload = () => {
|
||||||
|
inputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-24 h-24 overflow-hidden cursor-pointer ${className}`}
|
||||||
|
onClick={triggerUpload}
|
||||||
|
style={{
|
||||||
|
border: `1px solid ${token.colorBorder}`,
|
||||||
|
background: token.colorBgContainer,
|
||||||
|
...style, // 应用外部传入的样式
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={handleChange}
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
{previewUrl ? (
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="Avatar"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center w-full h-full text-sm text-gray-500">
|
||||||
|
{placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{file && file.status === "uploading" && (
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-white bg-opacity-75">
|
||||||
|
<Progress
|
||||||
|
percent={Math.round(
|
||||||
|
uploadProgress?.[file.fileKey!] || 0
|
||||||
|
)}
|
||||||
|
showInfo={false}
|
||||||
|
strokeColor={token.colorPrimary}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvatarUploader;
|
|
@ -1,241 +0,0 @@
|
||||||
// FileUploader.tsx
|
|
||||||
import React, { useRef, memo, useState } from "react";
|
|
||||||
import {
|
|
||||||
CloudArrowUpIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
DocumentIcon,
|
|
||||||
ExclamationCircleIcon,
|
|
||||||
CheckCircleIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
|
||||||
|
|
||||||
interface FileUploaderProps {
|
|
||||||
endpoint?: string;
|
|
||||||
onSuccess?: (result: { url: string; fileId: string }) => void;
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
maxSize?: number;
|
|
||||||
allowedTypes?: string[];
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileItemProps {
|
|
||||||
file: File;
|
|
||||||
progress?: number;
|
|
||||||
onRemove: (name: string) => void;
|
|
||||||
isUploaded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileItem: React.FC<FileItemProps> = memo(
|
|
||||||
({ file, progress, onRemove, isUploaded }) => (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -20 }}
|
|
||||||
className="group flex items-center p-4 bg-white rounded-xl border border-gray-100 shadow-sm hover:shadow-md transition-all duration-200">
|
|
||||||
<DocumentIcon className="w-8 h-8 text-blue-500/80" />
|
|
||||||
<div className="ml-3 flex-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-sm font-medium text-gray-900 truncate max-w-[200px]">
|
|
||||||
{file.name}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => onRemove(file.name)}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 rounded-full"
|
|
||||||
aria-label={`Remove ${file.name}`}>
|
|
||||||
<XMarkIcon className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{!isUploaded && progress !== undefined && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="bg-gray-100 rounded-full h-1.5 overflow-hidden">
|
|
||||||
<motion.div
|
|
||||||
className="bg-blue-500 h-1.5 rounded-full"
|
|
||||||
initial={{ width: 0 }}
|
|
||||||
animate={{ width: `${progress}%` }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 mt-1">
|
|
||||||
{progress}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isUploaded && (
|
|
||||||
<div className="mt-2 flex items-center text-green-500">
|
|
||||||
<CheckCircleIcon className="w-4 h-4 mr-1" />
|
|
||||||
<span className="text-xs">上传完成</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const FileUploader: React.FC<FileUploaderProps> = ({
|
|
||||||
onSuccess,
|
|
||||||
onError,
|
|
||||||
maxSize = 100,
|
|
||||||
placeholder = "点击或拖拽文件到这里上传",
|
|
||||||
allowedTypes = ["*/*"],
|
|
||||||
}) => {
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [files, setFiles] = useState<
|
|
||||||
Array<{ file: File; isUploaded: boolean }>
|
|
||||||
>([]);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const { progress, isUploading, uploadError, handleFileUpload } =
|
|
||||||
useTusUpload();
|
|
||||||
|
|
||||||
const handleError = (error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
onError?.(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrag = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.type === "dragenter" || e.type === "dragover") {
|
|
||||||
setIsDragging(true);
|
|
||||||
} else if (e.type === "dragleave") {
|
|
||||||
setIsDragging(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateFile = (file: File) => {
|
|
||||||
if (file.size > maxSize * 1024 * 1024) {
|
|
||||||
throw new Error(`文件大小不能超过 ${maxSize}MB`);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!allowedTypes.includes("*/*") &&
|
|
||||||
!allowedTypes.includes(file.type)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`不支持的文件类型。支持的类型: ${allowedTypes.join(", ")}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadFile = (file: File) => {
|
|
||||||
try {
|
|
||||||
validateFile(file);
|
|
||||||
handleFileUpload(
|
|
||||||
file,
|
|
||||||
(upload) => {
|
|
||||||
console.log("Upload complete:", {
|
|
||||||
url: upload.url,
|
|
||||||
fileId: upload.fileId,
|
|
||||||
// resource: upload.resource
|
|
||||||
});
|
|
||||||
onSuccess?.(upload);
|
|
||||||
setFiles((prev) =>
|
|
||||||
prev.map((f) =>
|
|
||||||
f.file.name === file.name
|
|
||||||
? { ...f, isUploaded: true }
|
|
||||||
: f
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
handleError
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error as Error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragging(false);
|
|
||||||
|
|
||||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
|
||||||
setFiles((prev) => [
|
|
||||||
...prev,
|
|
||||||
...droppedFiles.map((file) => ({ file, isUploaded: false })),
|
|
||||||
]);
|
|
||||||
droppedFiles.forEach(uploadFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (e.target.files) {
|
|
||||||
const selectedFiles = Array.from(e.target.files);
|
|
||||||
setFiles((prev) => [
|
|
||||||
...prev,
|
|
||||||
...selectedFiles.map((file) => ({ file, isUploaded: false })),
|
|
||||||
]);
|
|
||||||
selectedFiles.forEach(uploadFile);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFile = (fileName: string) => {
|
|
||||||
setFiles((prev) => prev.filter(({ file }) => file.name !== fileName));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full space-y-4">
|
|
||||||
<div
|
|
||||||
onClick={handleClick}
|
|
||||||
onDragEnter={handleDrag}
|
|
||||||
onDragLeave={handleDrag}
|
|
||||||
onDragOver={handleDrag}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
className={`
|
|
||||||
relative flex flex-col items-center justify-center w-full h-32
|
|
||||||
border-2 border-dashed rounded-lg cursor-pointer
|
|
||||||
transition-colors duration-200 ease-in-out
|
|
||||||
${
|
|
||||||
isDragging
|
|
||||||
? "border-blue-500 bg-blue-50"
|
|
||||||
: "border-gray-300 hover:border-blue-500"
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
accept={allowedTypes.join(",")}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<CloudArrowUpIcon className="w-10 h-10 text-gray-400" />
|
|
||||||
<p className="mt-2 text-sm text-gray-500">{placeholder}</p>
|
|
||||||
{isDragging && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-blue-100/50 rounded-lg">
|
|
||||||
<p className="text-blue-500 font-medium">
|
|
||||||
释放文件以上传
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{files.map(({ file, isUploaded }) => (
|
|
||||||
<FileItem
|
|
||||||
key={file.name}
|
|
||||||
file={file}
|
|
||||||
progress={isUploaded ? 100 : progress}
|
|
||||||
onRemove={removeFile}
|
|
||||||
isUploaded={isUploaded}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{uploadError && (
|
|
||||||
<div className="flex items-center text-red-500 text-sm">
|
|
||||||
<ExclamationCircleIcon className="w-4 h-4 mr-1" />
|
|
||||||
<span>{uploadError}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileUploader;
|
|
|
@ -96,24 +96,38 @@ export function UserMenu() {
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
className="relative rounded-full focus:outline-none
|
className="flex items-center rounded-full transition-all duration-200 ease-in-out">
|
||||||
focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2
|
{/* Avatar 容器,相对定位 */}
|
||||||
focus:ring-offset-white transition-all duration-200 ease-in-out">
|
<div className="relative">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={user?.avatar}
|
src={user?.avatar}
|
||||||
name={user?.showname || user?.username}
|
name={user?.showname || user?.username}
|
||||||
size={40}
|
size={40}
|
||||||
className="ring-2 ring-white hover:ring-[#00538E]/90
|
className="ring-2 ring-white hover:ring-[#00538E]/90
|
||||||
transition-all duration-200 ease-in-out shadow-md
|
transition-all duration-200 ease-in-out shadow-md
|
||||||
hover:shadow-lg"
|
hover:shadow-lg focus:outline-none
|
||||||
/>
|
focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2
|
||||||
<span
|
focus:ring-offset-white "
|
||||||
className="absolute bottom-0 right-0 h-3 w-3
|
/>
|
||||||
rounded-full bg-emerald-500 ring-2 ring-white
|
{/* 小绿点 */}
|
||||||
shadow-sm transition-transform duration-200
|
<span
|
||||||
ease-in-out hover:scale-110"
|
className="absolute bottom-0 right-0 h-3 w-3
|
||||||
aria-hidden="true"
|
rounded-full bg-emerald-500 ring-2 ring-white
|
||||||
/>
|
shadow-sm transition-transform duration-200
|
||||||
|
ease-in-out hover:scale-110"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用户信息,显示在 Avatar 右侧 */}
|
||||||
|
<div className="flex flex-col space-y-0.5 ml-3">
|
||||||
|
<span className="text-sm font-semibold text-white">
|
||||||
|
{user?.showname || user?.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-white flex items-center gap-1.5">
|
||||||
|
{user?.department?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import StaffList from "./staff-list";
|
import StaffList from "./staff-list";
|
||||||
import { ObjectType, RolePerms } from "@nice/common"
|
import { ObjectType, RolePerms } from "@nice/common";
|
||||||
import { Icon } from "@nice/iconer"
|
import { Icon } from "@nice/iconer";
|
||||||
import StaffModal from "./staff-modal";
|
import StaffModal from "./staff-modal";
|
||||||
import { createContext, useEffect, useMemo, useState } from "react";
|
import React,{ createContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { Button } from "antd";
|
import { Button } from "antd";
|
||||||
import DepartmentSelect from "../department/department-select";
|
import DepartmentSelect from "../department/department-select";
|
||||||
|
@ -10,63 +10,87 @@ import { FormInstance, useForm } from "antd/es/form/Form";
|
||||||
import AdminHeader from "../../layout/admin/AdminHeader";
|
import AdminHeader from "../../layout/admin/AdminHeader";
|
||||||
|
|
||||||
export const StaffEditorContext = createContext<{
|
export const StaffEditorContext = createContext<{
|
||||||
domainId: string,
|
domainId: string;
|
||||||
modalOpen: boolean,
|
modalOpen: boolean;
|
||||||
setDomainId: React.Dispatch<React.SetStateAction<string>>,
|
setDomainId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
editId: string,
|
editId: string;
|
||||||
setEditId: React.Dispatch<React.SetStateAction<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
|
canManageAnyStaff: boolean;
|
||||||
}>({
|
}>({
|
||||||
domainId: undefined,
|
domainId: undefined,
|
||||||
modalOpen: false,
|
modalOpen: false,
|
||||||
setDomainId: undefined,
|
setDomainId: undefined,
|
||||||
setModalOpen: undefined,
|
setModalOpen: undefined,
|
||||||
editId: undefined,
|
editId: undefined,
|
||||||
setEditId: undefined,
|
setEditId: undefined,
|
||||||
form: undefined,
|
form: undefined,
|
||||||
formLoading: undefined,
|
formLoading: undefined,
|
||||||
setFormLoading: undefined,
|
setFormLoading: undefined,
|
||||||
canManageAnyStaff: false
|
canManageAnyStaff: false,
|
||||||
});
|
});
|
||||||
export default function StaffEditor() {
|
export default function StaffEditor() {
|
||||||
const [form] = useForm()
|
const [form] = useForm();
|
||||||
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>()
|
const [formLoading, setFormLoading] = useState<boolean>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
setDomainId(user.domainId)
|
setDomainId(user.domainId);
|
||||||
}
|
}
|
||||||
}, [user])
|
}, [user]);
|
||||||
|
|
||||||
const canManageStaff = useMemo(() => {
|
const canManageStaff = useMemo(() => {
|
||||||
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF, RolePerms.MANAGE_DOM_STAFF)
|
return hasSomePermissions(
|
||||||
}, [user])
|
RolePerms.MANAGE_ANY_STAFF,
|
||||||
const canManageAnyStaff = useMemo(() => {
|
RolePerms.MANAGE_DOM_STAFF
|
||||||
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF)
|
);
|
||||||
}, [user])
|
}, [user]);
|
||||||
return <StaffEditorContext.Provider value={{ canManageAnyStaff, formLoading, setFormLoading, form, editId, setEditId, domainId, modalOpen, setDomainId, setModalOpen }}>
|
const canManageAnyStaff = useMemo(() => {
|
||||||
<AdminHeader roomId="staff-editor">
|
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF);
|
||||||
<div className="flex items-center gap-4">
|
}, [user]);
|
||||||
<DepartmentSelect rootId={user?.domainId} onChange={(value) => setDomainId(value as string)} disabled={!canManageAnyStaff} value={domainId} className="w-48" domain={true}></DepartmentSelect>
|
return (
|
||||||
{canManageStaff && <Button
|
<StaffEditorContext.Provider
|
||||||
|
value={{
|
||||||
type="primary"
|
canManageAnyStaff,
|
||||||
icon={<Icon name="add"></Icon>}
|
formLoading,
|
||||||
onClick={() => {
|
setFormLoading,
|
||||||
setModalOpen(true)
|
form,
|
||||||
}}>
|
editId,
|
||||||
添加用户
|
setEditId,
|
||||||
</Button>}
|
domainId,
|
||||||
</div>
|
modalOpen,
|
||||||
</AdminHeader>
|
setDomainId,
|
||||||
<StaffList domainId={domainId}></StaffList>
|
setModalOpen,
|
||||||
<StaffModal></StaffModal>
|
}}>
|
||||||
</StaffEditorContext.Provider>
|
<AdminHeader roomId="staff-editor">
|
||||||
}
|
<div className="flex items-center gap-4">
|
||||||
|
<DepartmentSelect
|
||||||
|
rootId={user?.domainId}
|
||||||
|
onChange={(value) => setDomainId(value as string)}
|
||||||
|
disabled={!canManageAnyStaff}
|
||||||
|
value={domainId}
|
||||||
|
className="w-48"
|
||||||
|
domain={true}></DepartmentSelect>
|
||||||
|
{canManageStaff && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Icon name="add"></Icon>}
|
||||||
|
onClick={() => {
|
||||||
|
setModalOpen(true);
|
||||||
|
}}>
|
||||||
|
添加用户
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AdminHeader>
|
||||||
|
<StaffList domainId={domainId}></StaffList>
|
||||||
|
<StaffModal></StaffModal>
|
||||||
|
</StaffEditorContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Button, Form, Input, Spin, Switch, message } from "antd";
|
import { Button, Form, Input, Spin, Switch, message } from "antd";
|
||||||
import { useContext, useEffect} from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { useStaff } from "@nice/client";
|
import { useStaff } from "@nice/client";
|
||||||
import DepartmentSelect from "../department/department-select";
|
import DepartmentSelect from "../department/department-select";
|
||||||
import { api } from "@nice/client"
|
import { api } from "@nice/client";
|
||||||
import { StaffEditorContext } from "./staff-editor";
|
import { StaffEditorContext } from "./staff-editor";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
import AvatarUploader from "../../common/uploader/AvatarUploader";
|
||||||
export default function StaffForm() {
|
export default function StaffForm() {
|
||||||
const { create, update } = useStaff(); // Ensure you have these methods in your hooks
|
const { create, update } = useStaff(); // Ensure you have these methods in your hooks
|
||||||
const {
|
const {
|
||||||
|
@ -31,8 +32,8 @@ export default function StaffForm() {
|
||||||
password,
|
password,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
officerId,
|
officerId,
|
||||||
enabled
|
enabled,
|
||||||
} = values
|
} = values;
|
||||||
setFormLoading(true);
|
setFormLoading(true);
|
||||||
try {
|
try {
|
||||||
if (data && editId) {
|
if (data && editId) {
|
||||||
|
@ -46,8 +47,8 @@ export default function StaffForm() {
|
||||||
password,
|
password,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
officerId,
|
officerId,
|
||||||
enabled
|
enabled,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await create.mutateAsync({
|
await create.mutateAsync({
|
||||||
|
@ -58,8 +59,8 @@ export default function StaffForm() {
|
||||||
domainId: fieldDomainId ? fieldDomainId : domainId,
|
domainId: fieldDomainId ? fieldDomainId : domainId,
|
||||||
password,
|
password,
|
||||||
officerId,
|
officerId,
|
||||||
phoneNumber
|
phoneNumber,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
if (deptId) form.setFieldValue("deptId", deptId);
|
if (deptId) form.setFieldValue("deptId", deptId);
|
||||||
|
@ -83,7 +84,7 @@ export default function StaffForm() {
|
||||||
form.setFieldValue("deptId", data.deptId);
|
form.setFieldValue("deptId", data.deptId);
|
||||||
form.setFieldValue("officerId", data.officerId);
|
form.setFieldValue("officerId", data.officerId);
|
||||||
form.setFieldValue("phoneNumber", data.phoneNumber);
|
form.setFieldValue("phoneNumber", data.phoneNumber);
|
||||||
form.setFieldValue("enabled", data.enabled)
|
form.setFieldValue("enabled", data.enabled);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -91,7 +92,7 @@ export default function StaffForm() {
|
||||||
form.setFieldValue("domainId", domainId);
|
form.setFieldValue("domainId", domainId);
|
||||||
form.setFieldValue("deptId", domainId);
|
form.setFieldValue("deptId", domainId);
|
||||||
}
|
}
|
||||||
}, [domainId, data]);
|
}, [domainId, data as any]);
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
@ -106,6 +107,16 @@ export default function StaffForm() {
|
||||||
requiredMark="optional"
|
requiredMark="optional"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onFinish={handleFinish}>
|
onFinish={handleFinish}>
|
||||||
|
<Form.Item
|
||||||
|
name={"meta.photoUrl"}
|
||||||
|
label="头像"
|
||||||
|
rules={[{ required: true }]}>
|
||||||
|
<AvatarUploader
|
||||||
|
style={{
|
||||||
|
width: "150px",
|
||||||
|
height: "200px",
|
||||||
|
}}></AvatarUploader>
|
||||||
|
</Form.Item>
|
||||||
{canManageAnyStaff && (
|
{canManageAnyStaff && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={"domainId"}
|
name={"domainId"}
|
||||||
|
@ -127,7 +138,8 @@ export default function StaffForm() {
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={"username"}
|
name={"username"}
|
||||||
label="帐号">
|
label="帐号">
|
||||||
<Input allowClear
|
<Input
|
||||||
|
allowClear
|
||||||
autoComplete="new-username" // 使用非标准的自动完成值
|
autoComplete="new-username" // 使用非标准的自动完成值
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
@ -136,7 +148,8 @@ export default function StaffForm() {
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={"showname"}
|
name={"showname"}
|
||||||
label="姓名">
|
label="姓名">
|
||||||
<Input allowClear
|
<Input
|
||||||
|
allowClear
|
||||||
autoComplete="new-name" // 使用非标准的自动完成值
|
autoComplete="new-name" // 使用非标准的自动完成值
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
@ -146,8 +159,8 @@ export default function StaffForm() {
|
||||||
{
|
{
|
||||||
required: false,
|
required: false,
|
||||||
pattern: /^\d{5,18}$/,
|
pattern: /^\d{5,18}$/,
|
||||||
message: "请输入正确的证件号(数字)"
|
message: "请输入正确的证件号(数字)",
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
name={"officerId"}
|
name={"officerId"}
|
||||||
label="证件号">
|
label="证件号">
|
||||||
|
@ -158,20 +171,29 @@ export default function StaffForm() {
|
||||||
{
|
{
|
||||||
required: false,
|
required: false,
|
||||||
pattern: /^\d{6,11}$/,
|
pattern: /^\d{6,11}$/,
|
||||||
message: "请输入正确的手机号(数字)"
|
message: "请输入正确的手机号(数字)",
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
name={"phoneNumber"}
|
name={"phoneNumber"}
|
||||||
label="手机号">
|
label="手机号">
|
||||||
<Input autoComplete="new-phone" // 使用非标准的自动完成值
|
<Input
|
||||||
spellCheck={false} allowClear />
|
autoComplete="new-phone" // 使用非标准的自动完成值
|
||||||
|
spellCheck={false}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="密码" name={"password"}>
|
<Form.Item label="密码" name={"password"}>
|
||||||
<Input.Password spellCheck={false} visibilityToggle autoComplete="new-password" />
|
<Input.Password
|
||||||
|
spellCheck={false}
|
||||||
|
visibilityToggle
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{editId && <Form.Item label="是否启用" name={"enabled"}>
|
{editId && (
|
||||||
<Switch></Switch>
|
<Form.Item label="是否启用" name={"enabled"}>
|
||||||
</Form.Item>}
|
<Switch></Switch>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue