Compare commits

...

12 Commits

79 changed files with 10052 additions and 10055 deletions

0
.vscode/settings.json vendored Normal file → Executable file
View File

View File

@ -21,20 +21,22 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
// 允许使用短路表达式
'no-unused-expressions': 'off',
// 允许使用 let 声明后不重新赋值的变量
'prefer-const': 'off',
// 允许使用 any 类型
'@typescript-eslint/no-explicit-any': 'off',
// 允许声明但未使用的变量
'@typescript-eslint/no-unused-vars': [
'warn',
{
vars: 'all', // 检查所有变量
args: 'none', // 不检查函数参数
vars: 'all',
args: 'none',
ignoreRestSiblings: true,
},
],
// 禁止使用未声明的变量
'no-undef': 'error',
// 可选:关闭未定义变量检查
'no-undef': 'off',
},
};

View File

View File

View File

View File

View File

@ -29,7 +29,6 @@ export class DepartmentService extends BaseTreeService<Prisma.DepartmentDelegate
// 如果未找到部门,返回空数组
if (!ancestorDepartment) return [];
// 查询同域下以指定部门为祖先的部门血缘关系
const departmentAncestries = await db.deptAncestry.findMany({
where: {
@ -335,4 +334,214 @@ export class DepartmentService extends BaseTreeService<Prisma.DepartmentDelegate
// 合并并去重父级和自身节点,返回唯一项
return getUniqueItems([...parents, ...selfItems], 'id');
}
/**
*
* @param options
* @returns
*/
private getDeptStatsCacheKey(options?: any): string {
return `dept_stats_${JSON.stringify(options || {})}_${Date.now()
.toString()
.slice(0, -4)}`;
}
/**
*
*
* @returns
*/
async getDepartmentStats() {
const cacheKey = this.getDeptStatsCacheKey();
const [departments, deptWithStaffCount] = await Promise.all([
db.department.findMany({
where: { deletedAt: null },
select: { id: true, name: true, parentId: true, isDomain: true },
}),
db.department.findMany({
where: { deletedAt: null },
select: {
id: true,
name: true,
_count: {
select: { deptStaffs: true },
},
},
}),
]);
// 处理数据
const stats = {
totalDepartments: departments.length,
departmentsWithStaff: deptWithStaffCount.filter(
(d) => d._count.deptStaffs > 0,
).length,
topDepartments: deptWithStaffCount
.sort((a, b) => b._count.deptStaffs - a._count.deptStaffs)
.slice(0, 10)
.map((d) => ({
id: d.id,
name: d.name,
staffCount: d._count.deptStaffs,
})),
domains: departments.filter((d) => d.isDomain).length,
departmentsByParent: this.groupDepartmentsByParent(departments),
};
return stats;
}
private groupDepartmentsByParent(departments: any[]): Record<string, any[]> {
const result: Record<string, any[]> = {};
departments.forEach((dept) => {
const parentId = dept.parentId || 'root';
if (!result[parentId]) {
result[parentId] = [];
}
result[parentId].push(dept);
});
return result;
}
/**
*
* @param options
* @returns
*/
async findManyOptimized(options: {
page?: number;
pageSize?: number;
orderBy?: any;
filter?: any;
}) {
const {
page = 1,
pageSize = 20,
orderBy = { order: 'asc' },
filter = {},
} = options;
// 构建查询条件
const where = {
deletedAt: null,
...filter,
};
// 并行执行总数查询和分页数据查询
const [total, items] = await Promise.all([
db.department.count({ where }),
db.department.findMany({
where,
orderBy,
skip: (page - 1) * pageSize,
take: pageSize,
select: {
id: true,
name: true,
parentId: true,
isDomain: true,
order: true,
domainId: true,
_count: {
select: { deptStaffs: true },
},
},
}),
]);
// 格式化结果
const formattedItems = items.map((item) => ({
id: item.id,
name: item.name,
parentId: item.parentId,
isDomain: item.isDomain,
order: item.order,
domainId: item.domainId,
staffCount: item._count.deptStaffs,
}));
return {
items: formattedItems,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
/**
*
* @param rootId ID
* @param lazy
* @returns
*/
async getDepartmentTree(rootId: string | null = null, lazy: boolean = false) {
// 基础查询条件
const baseWhere: any = { deletedAt: null };
// 如果是懒加载模式,只查询根节点的直接子节点
if (lazy && rootId) {
baseWhere.parentId = rootId;
}
// 如果不是懒加载且指定了rootId查询该节点及其所有子孙节点
else if (rootId) {
const descendantIds = await this.getDescendantIds([rootId], true);
baseWhere.id = { in: descendantIds };
}
// 查询部门数据
const departments = await db.department.findMany({
where: baseWhere,
select: {
id: true,
name: true,
parentId: true,
isDomain: true,
order: true,
_count: {
select: { children: true, deptStaffs: true },
},
},
orderBy: { order: 'asc' },
});
// 如果是懒加载模式,返回扁平结构
if (lazy) {
return departments.map((dept) => ({
id: dept.id,
name: dept.name,
parentId: dept.parentId,
isDomain: dept.isDomain,
hasChildren: dept._count.children > 0,
staffCount: dept._count.deptStaffs,
isLeaf: dept._count.children === 0,
}));
}
// 如果不是懒加载模式,构建完整树形结构
return this.buildDepartmentTree(departments);
}
/**
*
* @param departments
* @param parentId ID
* @returns
*/
private buildDepartmentTree(
departments: any[],
parentId: string | null = null,
) {
return departments
.filter((dept) => dept.parentId === parentId)
.map((dept) => ({
id: dept.id,
name: dept.name,
isDomain: dept.isDomain,
staffCount: dept._count.deptStaffs,
children: this.buildDepartmentTree(departments, dept.id),
}));
}
}

View File

0
apps/server/src/models/position/dailyTrain.module.ts Normal file → Executable file
View File

0
apps/server/src/models/position/dailyTrain.router.ts Normal file → Executable file
View File

0
apps/server/src/models/position/dailyTrain.service.ts Normal file → Executable file
View File

View File

0
apps/server/src/models/sys-logs/systemLog.module.ts Normal file → Executable file
View File

0
apps/server/src/models/sys-logs/systemLog.router.ts Normal file → Executable file
View File

11
apps/server/src/models/sys-logs/systemLog.service.ts Normal file → Executable file
View File

@ -5,8 +5,11 @@ import EventBus, { CrudOperation } from "@server/utils/event-bus";
@Injectable()
export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
protected readonly prismaClient: any;
constructor() {
super(db, ObjectType.SYSTEM_LOG, false); // 不自动处理更新时间和删除时间
super(db, ObjectType.SYSTEM_LOG, false);
this.prismaClient = db;
}
async create(args: Prisma.SystemLogCreateArgs) {
@ -31,20 +34,19 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
return super.findMany(args); // 放弃分页结构
}
// 添加分页查询方法
async findManyWithPagination({ page = 1, pageSize = 20, where = {}, ...rest }: any) {
const skip = (page - 1) * pageSize;
try {
const [items, total] = await Promise.all([
this.delegate.findMany({
this.prismaClient.systemLog.findMany({
where,
skip,
take: pageSize,
orderBy: { timestamp: 'desc' },
...rest
}),
this.delegate.count({ where })
this.prismaClient.systemLog.count({ where })
]);
return {
@ -119,7 +121,6 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
};
}
});
return { changes };
}

View File

View File

View File

View File

View File

View File

View File

View File

24
apps/server/src/upload/share-code.service.ts Normal file → Executable file
View File

@ -11,12 +11,15 @@ export class ShareCodeService {
// 生成8位分享码使用易读的字符
private readonly generateCode = customAlphabet(
'23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
8
8,
);
constructor(private readonly resourceService: ResourceService) {}
async generateShareCode(fileId: string, fileName?: string): Promise<GenerateShareCodeResponse> {
async generateShareCode(
fileId: string,
fileName?: string,
): Promise<GenerateShareCodeResponse> {
try {
// 检查文件是否存在
const resource = await this.resourceService.findUnique({
@ -46,9 +49,7 @@ export class ShareCodeService {
expiresAt,
isUsed: false,
// 只在没有现有文件名且提供了新文件名时才更新文件名
...(fileName && !existingShareCode.fileName
? { fileName }
: {})
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
},
});
} else {
@ -95,10 +96,10 @@ export class ShareCodeService {
}
// 标记分享码为已使用
await db.shareCode.update({
where: { id: shareCode.id },
data: { isUsed: true },
});
// await db.shareCode.update({
// where: { id: shareCode.id },
// data: { isUsed: true },
// });
// 记录使用日志
this.logger.log(`Share code ${code} used for file ${shareCode.fileId}`);
@ -117,10 +118,7 @@ export class ShareCodeService {
try {
const result = await db.shareCode.deleteMany({
where: {
OR: [
{ expiresAt: { lt: new Date() } },
{ isUsed: true },
],
OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }],
},
});

View File

@ -23,8 +23,16 @@ export default tseslint.config(
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
// 允许使用 any 类型
"@typescript-eslint/no-explicit-any": "off",
// 允许使用 let 声明后不重新赋值的变量
"no-unused-expressions": "off",
// 允许使用 let 声明后不重新赋值的变量
"prefer-const": "off",
// 允许声明但未使用的变量
"@typescript-eslint/no-unused-vars": [

View File

View File

@ -15,8 +15,8 @@ export const defaultFields = [
label: '性别',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '男', value: '' },
{ label: '女', value: '' },
],
group: '基本信息',
order: 7 },
@ -57,11 +57,7 @@ export const defaultFields = [
// 职务信息组
{ name: 'rank',
label: '衔职级别',
type: 'select',
options: [
{ label: '', value: '' },
{ label: '', value: '' },
],
type: 'text',
group: '职务信息',
order: 13 },
{ name: 'rankDate', label: '衔职时间', type: 'date', group: '职务信息', order: 14 },

View File

View File

View File

View File

284
apps/web/src/app/main/admin/deptsettingpage/page.tsx Normal file → Executable file
View File

@ -3,33 +3,55 @@ import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
import { useState, useRef, useCallback } from "react";
import { message, Progress, Button, Tabs, DatePicker } from "antd";
import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons";
import {env} from '../../../../env'
import {
UploadOutlined,
DeleteOutlined,
InboxOutlined,
} from "@ant-design/icons";
import { env } from "../../../../env";
const { TabPane } = Tabs;
export default function DeptSettingPage() {
const [uploadedFileId, setUploadedFileId] = useState<string>('');
const [uploadedFileName, setUploadedFileName] = useState<string>('');
const [uploadedFileId, setUploadedFileId] = useState<string>("");
const [uploadedFileName, setUploadedFileName] = useState<string>("");
const [fileNameMap, setFileNameMap] = useState<Record<string, string>>({});
const [uploadedFiles, setUploadedFiles] = useState<{id: string, name: string}[]>([]);
const [uploadedFiles, setUploadedFiles] = useState<
{ id: string; name: string }[]
>([]);
const [isDragging, setIsDragging] = useState(false);
const [expireTime, setExpireTime] = useState<Date | null>(null);
const dropRef = useRef<HTMLDivElement>(null);
// 使用您的 useTusUpload hook
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({
const { uploadProgress, isUploading, uploadError, handleFileUpload } =
useTusUpload({
onSuccess: (result) => {
setUploadedFileId(result.fileId);
setUploadedFileName(result.fileName);
message.success('文件上传成功');
message.success("文件上传成功");
},
onError: (error: Error) => {
message.error('上传失败:' + error.message);
}
message.error("上传失败:" + error.message);
},
});
// 清除已上传文件
const handleClearFile = () => {
setUploadedFileId("");
setUploadedFileName("");
setUploadedFiles([]);
setFileNameMap({});
};
// 处理文件上传
const handleFileSelect = async (file: File) => {
// 限制:如果已有上传文件,则提示用户
if (uploadedFiles.length > 0) {
message.warning("只能上传一个文件,请先删除已上传的文件");
return;
}
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
handleFileUpload(
@ -38,60 +60,90 @@ export default function DeptSettingPage() {
setUploadedFileId(result.fileId);
setUploadedFileName(result.fileName);
// 添加到已上传文件列表
setUploadedFiles(prev => [...prev, {id: result.fileId, name: file.name}]);
setUploadedFiles([{ id: result.fileId, name: file.name }]);
// 在前端保存文件名映射(用于当前会话)
setFileNameMap(prev => ({
...prev,
[result.fileId]: file.name
}));
setFileNameMap({
[result.fileId]: file.name,
});
// 上传成功后保存原始文件名到数据库
try {
console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId);
console.log(
"正在保存文件名到数据库:",
result.fileName,
"对应文件ID:",
result.fileId
);
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, {
method: 'POST',
const response = await fetch(
`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`,
{
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
fileId: result.fileId,
fileName: file.name
fileName: file.name,
}),
});
}
);
const responseText = await response.text();
console.log('保存文件名响应:', response.status, responseText);
console.log("保存文件名响应:", response.status, responseText);
if (!response.ok) {
console.error('保存文件名失败:', responseText);
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
console.error("保存文件名失败:", responseText);
message.warning("文件名保存失败,下载时可能无法显示原始文件名");
} else {
console.log('文件名保存成功:', file.name);
console.log("文件名保存成功:", file.name);
}
} catch (error) {
console.error('保存文件名请求失败:', error);
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
console.error("保存文件名请求失败:", error);
message.warning("文件名保存失败,下载时可能无法显示原始文件名");
}
message.success('文件上传成功');
message.success("文件上传成功");
},
(error) => {
message.error('上传失败:' + error.message);
message.error("上传失败:" + error.message);
},
fileKey
);
};
// 处理多个文件上传
// 处理多个文件上传 - 已移除
// const handleFilesUpload = (file: File) => {
// handleFileSelect(file);
// };
// 处理文件删除
const handleDeleteFile = async (fileId: string) => {
try {
// 可以添加删除文件的API调用
// const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/delete/${fileId}`, {
// method: 'DELETE'
// });
// if (!response.ok) {
// throw new Error('删除文件失败');
// }
// 无论服务器删除是否成功,前端都需要更新状态
setUploadedFiles([]);
setUploadedFileId("");
setUploadedFileName("");
setFileNameMap({});
message.success("文件已删除");
} catch (error) {
console.error("删除文件错误:", error);
message.error("删除文件失败");
}
};
// 拖拽相关处理函数
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
@ -119,10 +171,9 @@ export default function DeptSettingPage() {
handleFileSelect(e.dataTransfer.files[0]);
}, []);
// 处理分享码生成成功
const handleShareSuccess = (code: string) => {
message.success('分享码生成成功:' + code);
message.success("分享码生成成功:" + code);
// 可以在这里添加其他逻辑,比如保存到历史记录
};
@ -130,16 +181,18 @@ export default function DeptSettingPage() {
const handleValidSuccess = async (fileId: string, fileName: string) => {
try {
// 构建下载URL包含文件名参数
const downloadUrl = `/upload/download/${fileId}?fileName=${encodeURIComponent(fileName)}`;
const downloadUrl = `/upload/download/${fileId}?fileName=${encodeURIComponent(
fileName
)}`;
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error('文件下载失败');
throw new Error("文件下载失败");
}
// 创建下载链接
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
// 直接使用传入的 fileName
@ -151,23 +204,25 @@ export default function DeptSettingPage() {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success('文件下载开始');
message.success("文件下载开始");
} catch (error) {
console.error('下载失败:', error);
message.error('文件下载失败');
console.error("下载失败:", error);
message.error("文件下载失败");
}
};
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "20px" }}>
<h2></h2>
<Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload">
{/* 文件上传区域 */}
<div style={{ marginBottom: '40px' }}>
<div style={{ marginBottom: "40px" }}>
<h3></h3>
{/* 如果没有已上传文件,显示上传区域 */}
{uploadedFiles.length === 0 ? (
<div
ref={dropRef}
onDragEnter={handleDragEnter}
@ -175,85 +230,122 @@ export default function DeptSettingPage() {
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
padding: '20px',
border: `2px dashed ${isDragging ? '#1890ff' : '#d9d9d9'}`,
borderRadius: '8px',
textAlign: 'center',
backgroundColor: isDragging ? 'rgba(24, 144, 255, 0.05)' : 'transparent',
transition: 'all 0.3s',
marginBottom: '20px'
padding: "20px",
border: `2px dashed ${isDragging ? "#1890ff" : "#d9d9d9"}`,
borderRadius: "8px",
textAlign: "center",
backgroundColor: isDragging
? "rgba(24, 144, 255, 0.05)"
: "transparent",
transition: "all 0.3s",
marginBottom: "20px",
}}
>
<InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
<InboxOutlined
style={{
fontSize: "48px",
color: isDragging ? "#1890ff" : "#d9d9d9",
}}
/>
<p></p>
<p style={{ fontSize: '12px', color: '#888' }}></p>
<p style={{ fontSize: "12px", color: "#888" }}>
</p>
<input
type="file"
id="file-input"
style={{ display: 'none' }}
style={{ display: "none" }}
onChange={(e) => {
const file = e.target.files[0];
const file = e.target.files?.[0];
if (file) {
handleFileSelect(file);
}
}}
disabled={isUploading}
/>
<label
htmlFor="file-input"
style={{
display: 'inline-block',
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px'
display: "inline-block",
padding: "8px 16px",
backgroundColor: "#1890ff",
color: "white",
borderRadius: "4px",
cursor: "pointer",
marginTop: "10px",
}}
>
<UploadOutlined />
</label>
</div>
) : (
<div style={{ marginBottom: "20px" }}>
<div
style={{
padding: "10px",
backgroundColor: "#f6ffed",
border: "1px solid #b7eb8f",
borderRadius: "4px",
marginBottom: "10px",
}}
>
<p style={{ color: "#52c41a", margin: 0 }}>
</p>
</div>
</div>
)}
{/* 已上传文件列表 */}
{uploadedFiles.length > 0 && (
<div style={{
border: '1px solid #f0f0f0',
borderRadius: '4px',
overflow: 'hidden'
}}>
{uploadedFiles.map((file, index) => (
<div key={file.id} style={{
display: 'flex',
alignItems: 'center',
padding: '10px 15px',
borderBottom: index < uploadedFiles.length - 1 ? '1px solid #f0f0f0' : 'none',
backgroundColor: index % 2 === 0 ? '#fafafa' : 'white'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
flex: 1
}}>
<div style={{
width: '20px',
height: '20px',
borderRadius: '50%',
backgroundColor: '#52c41a',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '10px'
}}>
<span style={{ color: 'white', fontSize: '12px' }}></span>
<div
style={{
border: "1px solid #f0f0f0",
borderRadius: "4px",
overflow: "hidden",
}}
>
{uploadedFiles.map((file) => (
<div
key={file.id}
style={{
display: "flex",
alignItems: "center",
padding: "10px 15px",
backgroundColor: "#fafafa",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
flex: 1,
}}
>
<div
style={{
width: "20px",
height: "20px",
borderRadius: "50%",
backgroundColor: "#52c41a",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginRight: "10px",
}}
>
<span style={{ color: "white", fontSize: "12px" }}>
</span>
</div>
<span>{file.name}</span>
</div>
<Button
type="text"
icon={<DeleteOutlined style={{ color: '#ff4d4f' }} />}
icon={<DeleteOutlined style={{ color: "#ff4d4f" }} />}
onClick={() => handleDeleteFile(file.id)}
title="删除此文件"
/>
</div>
))}
@ -261,7 +353,7 @@ export default function DeptSettingPage() {
)}
{isUploading && (
<div style={{ marginTop: '20px' }}>
<div style={{ marginTop: "20px" }}>
<Progress
percent={Math.round(Object.values(uploadProgress)[0] || 0)}
status="active"
@ -270,7 +362,7 @@ export default function DeptSettingPage() {
)}
{uploadError && (
<div style={{ color: '#ff4d4f', marginTop: '10px' }}>
<div style={{ color: "#ff4d4f", marginTop: "10px" }}>
{uploadError}
</div>
)}
@ -278,7 +370,7 @@ export default function DeptSettingPage() {
{/* 生成分享码区域 */}
{uploadedFileId && (
<div style={{ marginBottom: '40px' }}>
<div style={{ marginBottom: "40px" }}>
<h3></h3>
<ShareCodeGenerator
@ -293,9 +385,7 @@ export default function DeptSettingPage() {
<TabPane tab="下载文件" key="download">
<div>
<h3>使</h3>
<ShareCodeValidator
onValidSuccess={handleValidSuccess}
/>
<ShareCodeValidator onValidSuccess={handleValidSuccess} />
</div>
</TabPane>
</Tabs>

View File

View File

View File

View File

0
apps/web/src/app/main/daily/page.tsx Normal file → Executable file
View File

View File

@ -0,0 +1,66 @@
import React from "react";
import { Row, Col, Card, Radio, Button, Space, Typography } from "antd";
import { ReloadOutlined } from "@ant-design/icons";
const { Text } = Typography;
interface DeptDistribution {
id: string;
name: string;
count: number;
}
interface ChartControlsProps {
chartView: string;
setChartView: (view: string) => void;
selectedDept: string | null;
resetFilter: () => void;
deptDistribution: DeptDistribution[];
}
export default function ChartControls({
chartView,
setChartView,
selectedDept,
resetFilter,
deptDistribution,
}: ChartControlsProps): React.ReactElement {
return (
<Row className="mb-4">
<Col span={24}>
<Card className="shadow-sm">
<div className="flex justify-between items-center flex-wrap">
<Space>
<Text strong>: </Text>
<Radio.Group
value={chartView}
onChange={(e) => setChartView(e.target.value)}
optionType="button"
buttonStyle="solid"
>
<Radio.Button value="all"></Radio.Button>
<Radio.Button value="pie"></Radio.Button>
<Radio.Button value="bar"></Radio.Button>
</Radio.Group>
</Space>
<Space>
{selectedDept && (
<Button icon={<ReloadOutlined />} onClick={resetFilter}>
</Button>
)}
<Text type="secondary">
{selectedDept
? `已筛选: ${
deptDistribution.find((d) => d.id === selectedDept)
?.name || ""
}`
: "点击饼图可筛选部门"}
</Text>
</Space>
</div>
</Card>
</Col>
</Row>
);
}

View File

@ -0,0 +1,139 @@
import React, { lazy, Suspense, useRef, RefObject } from "react";
import { Row, Col, Spin, Empty, Button } from "antd";
import {
BarChartOutlined,
PieChartOutlined,
DownloadOutlined,
} from "@ant-design/icons";
import DashboardCard from "../../../components/presentation/dashboard-card";
import { message } from "antd";
import { EChartsOption } from "echarts-for-react";
// 懒加载图表组件
const ReactECharts = lazy(() => import("echarts-for-react"));
interface DeptData {
id: string;
name: string;
count: number;
}
interface DepartmentChartsProps {
chartView: string;
filteredDeptData: DeptData[];
pieOptions: EChartsOption;
barOptions: EChartsOption;
handleDeptSelection: (params: any) => void;
handleExportChart: (chartType: string, chartRef: RefObject<any>) => void;
}
export default function DepartmentCharts({
chartView,
filteredDeptData,
pieOptions,
barOptions,
handleDeptSelection,
handleExportChart,
}: DepartmentChartsProps): React.ReactElement {
const pieChartRef = useRef(null);
const barChartRef = useRef(null);
return (
<Row gutter={[16, 16]}>
{/* 条形图 */}
{(chartView === "all" || chartView === "bar") && (
<Col
xs={24}
sm={24}
md={chartView === "all" ? 12 : 24}
lg={chartView === "all" ? 12 : 24}
xl={chartView === "all" ? 12 : 24}
>
<DashboardCard
title={
<div className="flex items-center justify-between w-full">
<div>
<BarChartOutlined className="mr-2" />
<span> - </span>
</div>
<Button
type="text"
icon={<DownloadOutlined />}
onClick={() => handleExportChart("bar", barChartRef)}
size="small"
>
</Button>
</div>
}
className="min-h-96"
contentClassName="flex items-center justify-center"
>
{filteredDeptData.length > 0 ? (
<Suspense fallback={<Spin />}>
<ReactECharts
ref={barChartRef}
option={barOptions}
style={{ height: "360px", width: "100%" }}
notMerge={true}
lazyUpdate={true}
/>
</Suspense>
) : (
<Empty description="暂无部门数据" className="my-8" />
)}
</DashboardCard>
</Col>
)}
{/* 饼图 */}
{(chartView === "all" || chartView === "pie") && (
<Col
xs={24}
sm={24}
md={chartView === "all" ? 12 : 24}
lg={chartView === "all" ? 12 : 24}
xl={chartView === "all" ? 12 : 24}
>
<DashboardCard
title={
<div className="flex items-center justify-between w-full">
<div>
<PieChartOutlined className="mr-2" />
<span> - </span>
</div>
<Button
type="text"
icon={<DownloadOutlined />}
onClick={() => handleExportChart("pie", pieChartRef)}
size="small"
>
</Button>
</div>
}
className="min-h-96"
contentClassName="flex items-center justify-center"
>
{filteredDeptData.length > 0 ? (
<Suspense fallback={<Spin />}>
<ReactECharts
ref={pieChartRef}
option={pieOptions}
style={{ height: "360px", width: "100%" }}
notMerge={true}
lazyUpdate={true}
onEvents={{
click: handleDeptSelection,
}}
/>
</Suspense>
) : (
<Empty description="暂无部门数据" className="my-8" />
)}
</DashboardCard>
</Col>
)}
</Row>
);
}

View File

@ -0,0 +1,92 @@
import React from 'react';
import { Row, Col, Table, Badge, Empty, Tooltip } from "antd";
import { TeamOutlined, InfoCircleOutlined } from "@ant-design/icons";
import DashboardCard from "../../../components/presentation/dashboard-card";
import { theme } from "antd";
interface DeptData {
id: string;
name: string;
count: number;
}
interface DepartmentTableProps {
filteredDeptData: DeptData[];
staffs: any[] | undefined;
}
export default function DepartmentTable({
filteredDeptData,
staffs
}: DepartmentTableProps): React.ReactElement {
const { token } = theme.useToken();
const deptColumns = [
{
title: "部门名称",
dataIndex: "name",
key: "name",
},
{
title: "人员数量",
dataIndex: "count",
key: "count",
render: (count: number) => (
<Badge
count={count}
overflowCount={999}
style={{ backgroundColor: token.colorPrimary }}
/>
),
},
{
title: "占比",
dataIndex: "count",
key: "percentage",
render: (count: number) => (
<span>
{staffs && staffs.length > 0
? ((count / staffs.length) * 100).toFixed(1) + "%"
: "0%"}
</span>
),
},
];
return (
<Row className="mt-4">
<Col span={24}>
<DashboardCard
title={
<div className="flex items-center justify-between w-full">
<div>
<TeamOutlined className="mr-2" />
<span></span>
</div>
<Tooltip title="点击饼图可筛选特定部门数据">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
}
className="min-h-80"
>
{filteredDeptData.length > 0 ? (
<Table
dataSource={filteredDeptData}
columns={deptColumns}
rowKey="id"
pagination={{
pageSize: 10,
showTotal: (total) => `${total} 个部门`,
}}
size="small"
className="mt-2"
/>
) : (
<Empty description="暂无部门数据" className="my-8" />
)}
</DashboardCard>
</Col>
</Row>
);
}

View File

@ -0,0 +1,97 @@
import React from 'react';
import { Row, Col, Statistic, Tooltip } from "antd";
import { UserOutlined, TeamOutlined, IdcardOutlined, InfoCircleOutlined } from "@ant-design/icons";
import DashboardCard from "../../../components/presentation/dashboard-card";
import { theme } from "antd";
interface PositionStats {
total: number;
distribution: any[];
topPosition: {name: string; count: number} | null;
vacantPositions: number;
}
interface StatisticCardsProps {
staffs: any[] | undefined;
departments: any[] | undefined;
positionStats: PositionStats;
}
export default function StatisticCards({
staffs,
departments,
positionStats
}: StatisticCardsProps): React.ReactElement {
const { token } = theme.useToken();
return (
<Row gutter={[16, 16]} className="mb-6">
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<DashboardCard
title={
<div className="flex items-center">
<span></span>
<Tooltip title="系统中所有员工的总数">
<InfoCircleOutlined className="ml-2 text-gray-400" />
</Tooltip>
</div>
}
className="h-32"
>
<Statistic
value={staffs ? staffs.length : 0}
valueStyle={{ color: token.colorPrimary }}
prefix={<UserOutlined />}
/>
</DashboardCard>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<DashboardCard
title={
<div className="flex items-center">
<span></span>
<Tooltip title="系统中所有部门的数量">
<InfoCircleOutlined className="ml-2 text-gray-400" />
</Tooltip>
</div>
}
className="h-32"
>
<Statistic
value={Array.isArray(departments) ? departments.length : 0}
valueStyle={{ color: token.colorSuccess }}
prefix={<TeamOutlined />}
/>
</DashboardCard>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<DashboardCard
title={
<div className="flex items-center">
<span></span>
<Tooltip title="员工岗位类型统计">
<InfoCircleOutlined className="ml-2 text-gray-400" />
</Tooltip>
</div>
}
className="h-32"
>
<div className="flex flex-col">
<Statistic
value={positionStats.total || 0}
valueStyle={{ color: "#722ed1" }}
prefix={<IdcardOutlined />}
suffix={`种岗位`}
/>
{positionStats.topPosition && (
<div className="mt-2 text-sm text-gray-500">
: {positionStats.topPosition.name} (
{positionStats.topPosition.count})
</div>
)}
</div>
</DashboardCard>
</Col>
</Row>
);
}

View File

@ -0,0 +1,111 @@
import { EChartsOption } from "echarts-for-react";
interface DeptData {
name: string;
count: number;
id: string;
}
// 图表配置函数
export const getPieChartOptions = (
deptData: DeptData[],
onEvents?: Record<string, (params: any) => void>
): EChartsOption => {
const top10Depts = deptData.slice(0, 10);
return {
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b}: {c}人 ({d}%)",
},
legend: {
orient: "vertical",
right: 10,
top: "center",
data: top10Depts.map((dept) => dept.name),
},
series: [
{
name: "部门人数",
type: "pie",
radius: ["50%", "70%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: "#fff",
borderWidth: 2,
},
label: {
show: true,
formatter: "{b}: {c}人 ({d}%)",
position: "outside",
},
emphasis: {
label: {
show: true,
fontSize: "18",
fontWeight: "bold",
},
},
labelLine: {
show: true,
},
data: top10Depts.map((dept) => ({
value: dept.count,
name: dept.name,
})),
},
],
};
};
export const getBarChartOptions = (deptData: DeptData[]): EChartsOption => {
const top10Depts = deptData.slice(0, 10);
return {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
grid: {
left: "3%",
right: "12%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "value",
boundaryGap: [0, 0.01],
},
yAxis: {
type: "category",
data: top10Depts.map((dept) => dept.name),
inverse: true,
},
series: [
{
name: "人员数量",
type: "bar",
data: top10Depts.map((dept) => dept.count),
itemStyle: {
color: function (params) {
const colorList = [
"#91cc75",
"#5470c6",
"#ee6666",
"#73c0de",
"#3ba272",
"#fc8452",
"#9a60b4",
];
return colorList[params.dataIndex % colorList.length];
},
},
label: {
show: true,
position: "right",
formatter: "{c}人",
},
},
],
};
};

View File

@ -1,9 +1,213 @@
import React from "react"
import React, { useEffect, useState, useMemo, RefObject } from "react";
import { Spin, message } from "antd";
import { api } from "@nice/client";
export default function Dashboard() {
return (
<div >
</div>
)
import StatisticCards from "./StatisticCards";
import ChartControls from "./ChartControls";
import DepartmentCharts from "./DepartmentCharts";
import DepartmentTable from "./DepartmentTable";
import { getPieChartOptions, getBarChartOptions } from "./char-options";
interface DeptData {
id: string;
name: string;
count: number;
}
interface PositionData {
name: string;
count: number;
}
interface PositionStats {
total: number;
distribution: PositionData[];
topPosition: PositionData | null;
vacantPositions: number;
}
export default function Dashboard(): React.ReactElement {
// 获取员工数据
const { data: staffs, isLoading: staffLoading } = api.staff.findMany.useQuery(
{}
);
// 获取部门数据
const { data: departments, isLoading: deptLoading } =
api.department.findMany.useQuery({});
// 部门人员分布
const [deptDistribution, setDeptDistribution] = useState<DeptData[]>([]);
// 选中的部门筛选
const [selectedDept, setSelectedDept] = useState<string | null>(null);
// 图表视图类型
const [chartView, setChartView] = useState<string>("all");
// 岗位统计状态
const [positionStats, setPositionStats] = useState<PositionStats>({
total: 0,
distribution: [],
topPosition: null,
vacantPositions: 0,
});
// 处理原始数据,提取部门和岗位分布信息
useEffect(() => {
if (staffs && departments) {
// 计算部门分布
const deptMap = new Map<string, DeptData>();
if (Array.isArray(departments)) {
departments.forEach((dept) => {
deptMap.set(dept.id, { name: dept.name, count: 0, id: dept.id });
});
}
staffs.forEach((staff) => {
if (staff.deptId && deptMap.has(staff.deptId)) {
const deptData = deptMap.get(staff.deptId);
if (deptData) {
deptData.count += 1;
deptMap.set(staff.deptId, deptData);
}
}
});
// 部门数据排序
const deptArray = Array.from(deptMap.values())
.filter((dept) => dept.count > 0)
.sort((a, b) => b.count - a.count);
setDeptDistribution(deptArray);
// 岗位分布统计
const positionMap = new Map<string, PositionData>();
staffs.forEach((staff) => {
const position = staff.positionId || "未设置岗位";
if (!positionMap.has(position)) {
positionMap.set(position, { name: position, count: 0 });
}
const posData = positionMap.get(position);
if (posData) {
posData.count += 1;
positionMap.set(position, posData);
}
});
// 转换为数组并排序
const positionArray = Array.from(positionMap.values()).sort(
(a, b) => b.count - a.count
);
// 找出人数最多的岗位
const topPosition = positionArray.length > 0 ? positionArray[0] : null;
// 计算空缺岗位数(简化示例,实际可能需要根据业务逻辑调整)
const vacantPositions = positionArray.filter((p) => p.count === 0).length;
// 更新岗位统计状态
setPositionStats({
total: positionMap.size,
distribution: positionArray,
topPosition,
vacantPositions,
});
}
}, [staffs, departments]);
// 过滤部门数据
const filteredDeptData = useMemo(() => {
if (selectedDept) {
return deptDistribution.filter((dept) => dept.id === selectedDept);
}
return deptDistribution;
}, [deptDistribution, selectedDept]);
// 图表配置
const pieOptions = useMemo(() => {
return getPieChartOptions(filteredDeptData, {});
}, [filteredDeptData]);
const barOptions = useMemo(() => {
return getBarChartOptions(filteredDeptData);
}, [filteredDeptData]);
// 处理部门选择
const handleDeptSelection = (params: any) => {
const selectedDeptName = params.name;
const selectedDept = deptDistribution.find(
(dept) => dept.name === selectedDeptName
);
if (selectedDept) {
setSelectedDept(selectedDept.id);
message.info(`已选择部门: ${selectedDeptName}`);
}
};
// 导出图表为图片
const handleExportChart = (chartType: string, chartRef: RefObject<any>) => {
if (chartRef.current && chartRef.current.getEchartsInstance) {
const chart = chartRef.current.getEchartsInstance();
const dataURL = chart.getDataURL({
type: "png",
pixelRatio: 2,
backgroundColor: "#fff",
});
const link = document.createElement("a");
link.download = `部门人员分布-${
chartType === "pie" ? "饼图" : "条形图"
}.png`;
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success(`${chartType === "pie" ? "饼图" : "条形图"}导出成功`);
}
};
// 重置筛选
const resetFilter = () => {
setSelectedDept(null);
message.success("已重置筛选");
};
const isLoading = staffLoading || deptLoading;
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-6"></h1>
{isLoading ? (
<div className="flex justify-center items-center h-64">
<Spin size="large" />
</div>
) : (
<>
{/* 统计卡片 */}
<StatisticCards
staffs={staffs}
departments={departments}
positionStats={positionStats}
/>
{/* 图表筛选和控制区 */}
<ChartControls
chartView={chartView}
setChartView={setChartView}
selectedDept={selectedDept}
resetFilter={resetFilter}
deptDistribution={deptDistribution}
/>
{/* 部门分布数据可视化 */}
<DepartmentCharts
chartView={chartView}
filteredDeptData={filteredDeptData}
pieOptions={pieOptions}
barOptions={barOptions}
handleExportChart={handleExportChart}
handleDeptSelection={handleDeptSelection}
/>
{/* 详细数据表格 */}
<DepartmentTable
staffs={staffs}
filteredDeptData={filteredDeptData}
/>
</>
)}
</div>
);
}

16
apps/web/src/app/main/layout/MainHeader.tsx Normal file → Executable file
View File

@ -1,19 +1,18 @@
import { Layout, Avatar, Dropdown, Menu } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
import NavigationMenu from './NavigationMenu';
import { Header } from 'antd/es/layout/layout';
import { useState } from 'react';
import { Layout, Avatar, Dropdown, Menu } from "antd";
import { UserOutlined } from "@ant-design/icons";
import { useNavigate, Outlet, useLocation } from "react-router-dom";
import NavigationMenu from "./NavigationMenu";
import { Header } from "antd/es/layout/layout";
import { useState } from "react";
const { Sider, Content } = Layout;
export default function MainHeader() {
return (
<Layout>
{/* 顶部Header */}
<Header className="flex justify-end items-center bg-white shadow-sm h-16 px-6">
<Avatar className='bg-black'></Avatar>
<Avatar className="bg-black"></Avatar>
</Header>
{/* 主体布局 */}
@ -22,7 +21,6 @@ export default function MainHeader() {
<Sider theme="light" width={240} className="shadow-sm">
<NavigationMenu />
</Sider>
{/* 内容区域 */}
<Content className="overflow-auto p-6 bg-gray-50">
<Outlet />

0
apps/web/src/app/main/layout/MainLayout.tsx Normal file → Executable file
View File

0
apps/web/src/app/main/layout/MainProvider.tsx Normal file → Executable file
View File

0
apps/web/src/app/main/layout/NavigationMenu.tsx Normal file → Executable file
View File

0
apps/web/src/app/main/plan/monthplan/page.tsx Normal file → Executable file
View File

0
apps/web/src/app/main/plan/weekplan/page.tsx Normal file → Executable file
View File

0
apps/web/src/app/main/staffinfo_show/page.tsx Normal file → Executable file
View File

View File

@ -1,10 +1,47 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-unused-expressions */
/* eslint-disable no-unused-expressions */
import { useState, useEffect, useMemo, useCallback } from "react";
import { AgGridReact } from "ag-grid-react";
import { api, useStaff } from "@nice/client";
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import { SetFilterModule } from 'ag-grid-enterprise';
import { Button, message } from 'antd';
import { Button, CascaderProps, message, Modal, Input, Upload } from "antd";
import { areaOptions } from "@web/src/app/main/staffinfo_write/area-options";
import StaffInfoWrite from "@web/src/app/main/staffinfo_write/staffinfo_write.page";
import { utils, writeFile, read } from "xlsx";
import { UploadOutlined } from "@ant-design/icons";
function getAreaName(codes: string[], level?: number): string {
const result: string[] = [];
let currentLevel: CascaderProps["options"] = areaOptions;
for (const code of codes) {
const found = currentLevel?.find((opt) => opt.value === code);
if (!found) break;
result.push(String(found.label));
currentLevel = found.children || [];
if (level && result.length >= level) break; // 添加层级控制
}
return level
? result[level - 1] || ""
: result.join(" / ") || codes.join("/");
}
// 添加表头提取工具函数
function extractHeaders(columns: any[]): string[] {
const result: string[] = [];
const extractHeadersRecursive = (cols: any[]) => {
cols.forEach((col) => {
if ("children" in col && col.children) {
extractHeadersRecursive(col.children);
} else if (col.headerName) {
result.push(col.headerName);
}
});
};
extractHeadersRecursive(columns);
return result;
}
export default function StaffMessage() {
const [rowData, setRowData] = useState<any[]>([]);
@ -12,88 +49,134 @@ export default function StaffMessage() {
const [selectedRows, setSelectedRows] = useState<any[]>([]);
const { useCustomFields, softDeleteByIds } = useStaff();
const fields = useCustomFields();
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [currentEditStaff, setCurrentEditStaff] = useState<any>(null);
const [gridApi, setGridApi] = useState<any>(null);
const [fileNameVisible, setFileNameVisible] = useState(false);
const [fileName, setFileName] = useState("");
const [defaultFileName] = useState(
`员工数据_${new Date().toISOString().slice(0, 10)}`
);
const [importVisible, setImportVisible] = useState(false);
// 获取数据
const { data: staffData } = api.staff.findMany.useQuery({
where: {
deletedAt: null
deletedAt: null,
},
include: {
fieldValues: {
include: {
// 添加这两个关联字段
staff: { select: { id: true } }, // 关联员工ID
field: { select: { id: true } } // 关联字段ID
}
field: { select: { id: true } }, // 关联字段ID
},
},
department: true,
},
department: true
}
} as any);
const actionColumns = [{
// console.log(staffData);
const actionColumns = [
{
field: "action",
width: 50,
checkboxSelection: true,
headerCheckboxSelection: true,
pinned: 'left'
}]
pinned: "left",
},
];
// 新增编辑处理函数
const handleEdit = useCallback(async () => {
if (selectedRows.length === 0) return;
if (selectedRows.length > 1) {
message.error('只能选择一个员工进行编辑');
message.error("只能选择一个员工进行编辑");
return;
}
console.log(selectedRows[0]);
setCurrentEditStaff(selectedRows[0]);
setIsEditModalVisible(true);
}, [selectedRows]);
// 处理编辑完成
const handleEditComplete = useCallback(() => {
setIsEditModalVisible(false);
setCurrentEditStaff(null);
// 刷新表格数据
api.staff.findMany.useQuery();
}, []);
// 新增删除处理函数
const handleDelete = useCallback(async () => {
if (selectedRows.length === 0) return;
console.log("待删除的选中行数据:", selectedRows); // 新增调试语句
try {
await softDeleteByIds.mutateAsync({
ids: selectedRows?.map(row => row.id)
ids: selectedRows?.map((row) => {
console.log("当前行ID:", row.id); // 检查每个ID
return row.id;
}),
});
message.success('删除成功');
message.success("删除成功");
} catch (error) {
message.error('删除失败');
message.error("删除失败");
console.error("详细错误信息:", error); // 输出完整错误堆栈
}
// 重新获取数据或本地过滤已删除项
}, [selectedRows]);
// 缓存基础列定义
const baseColumns = useMemo(() => [
const baseColumns = useMemo(
() => [
{
field: 'showname',
headerName: '姓名',
filter: 'agSetColumnFilter',
pinned: 'left'
field: "showname",
headerName: "姓名",
filter: "agSetColumnFilter",
pinned: "left",
},
{
field: 'deptId',
headerName: '所属部门',
valueGetter: params => params.data.department?.name,
filter: 'agSetColumnFilter'
}
], []);
field: "deptId",
headerName: "所属部门",
valueGetter: (params) => params.data.department?.name,
filter: "agSetColumnFilter",
},
],
[]
);
// 缓存动态列定义
const dynamicColumns = useMemo(() =>
(fields.data || [] as any).map(field => ({
const dynamicColumns = useMemo(
() =>
(fields.data || ([] as any)).map((field) => ({
field: field.name,
headerName: field.label || field.name,
filter: 'agSetColumnFilter',
valueGetter: params => {
// 从 fieldValues 对象中获取值
return params.data.fieldValues?.find(
filter: "agSetColumnFilter",
cellStyle: { whiteSpace: "pre-line" },
autoHeight: true,
valueGetter: (params) => {
// 获取原始值
const rawValue = params.data.fieldValues?.find(
(fv: any) => fv.fieldId === field.id
)?.value;
// 根据字段类型格式化
switch (field.type) {
case "cascader":
return rawValue ? getAreaName(rawValue.split("/")) : "";
case "date":
// 格式化日期假设存储的是ISO字符串
return rawValue ? new Date(rawValue).toLocaleDateString() : "";
case "textarea":
// 换行处理
return rawValue?.replace(/,/g, "\n");
default:
return rawValue;
}
},
})),
[fields.data]
);
// 合并列定义
useEffect(() => {
setColumnDefs([...actionColumns, ...baseColumns, ...dynamicColumns]);
@ -104,27 +187,401 @@ export default function StaffMessage() {
staffData && setRowData(staffData);
}, [staffData]);
return (
// 修改导出模板处理函数
const handleExportTemplate = useCallback(() => {
const headerNames = extractHeaders(columnDefs);
// 创建示例数据行
const exampleRow: Record<string, string> = {};
// 定义 fieldsList移到这里
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
// 检查是否有选中行
if (selectedRows.length > 0) {
// 使用第一条选中的记录作为模板数据
const templateData = selectedRows[0];
// 基础字段
exampleRow["姓名"] = templateData.showname || "";
exampleRow["所属部门"] = templateData.department?.name || "";
// 处理自定义字段
fieldsList.forEach((field: any) => {
const fieldValue = templateData.fieldValues?.find(
(fv: any) => fv.fieldId === field.id
)?.value;
let displayValue = fieldValue;
// 根据字段类型处理值
if (field.type === "cascader" && fieldValue) {
displayValue = getAreaName(fieldValue.split("/"));
} else if (field.type === "date" && fieldValue) {
displayValue = new Date(fieldValue).toLocaleDateString();
} else if (field.type === "textarea" && fieldValue) {
displayValue = fieldValue.replace(/,/g, "\n");
}
exampleRow[field.label || field.name] = displayValue || "";
});
} else {
// 如果没有选中行,使用默认示例数据
exampleRow["姓名"] = "张三";
exampleRow["所属部门"] = "技术部";
// 添加所有自定义字段的空值
fieldsList.forEach((field: any) => {
exampleRow[field.label || field.name] = "";
});
}
// 创建空白行供用户填写
const emptyRow = headerNames.reduce(
(obj, header) => {
obj[header] = "";
return obj;
},
{} as Record<string, string>
);
// 创建工作簿和工作表
const wb = utils.book_new();
const ws = utils.json_to_sheet([exampleRow], { header: headerNames });
// 设置列宽
const colWidth = headerNames.map(() => ({ wch: 20 }));
ws["!cols"] = colWidth;
// 在第二行添加提示文字
const rowIdx = 2; // 第二行索引
const cellRef = utils.encode_cell({ r: rowIdx, c: 0 }); // A3单元格
const tipText =
selectedRows.length > 0
? "以上为选中人员数据,请在下方行填写实际数据"
: "以上为示例数据,请在下方行填写实际数据";
ws[cellRef] = { t: "s", v: tipText };
// 手动添加空白行
utils.sheet_add_json(ws, [emptyRow], {
skipHeader: true,
origin: rowIdx + 1,
});
// 合并提示文字单元格
if (!ws["!merges"]) ws["!merges"] = [];
ws["!merges"].push({
s: { r: rowIdx, c: 0 }, // 起始单元格 A3
e: { r: rowIdx, c: Math.min(5, headerNames.length - 1) }, // 结束单元格,跨越多列
});
utils.book_append_sheet(wb, ws, "员工模板");
writeFile(wb, `员工数据模板_${new Date().toISOString().slice(0, 10)}.xlsx`);
}, [columnDefs, selectedRows, fields.data]);
// 导出数据处理函数
const handleConfirm = useCallback(() => {
setFileNameVisible(true);
}, []);
// 处理文件名确认
const handleFileNameConfirm = useCallback(() => {
setFileNameVisible(false);
if (!gridApi) {
console.error("Grid API 未正确初始化");
return;
}
try {
// 获取所有数据
const allData = selectedRows.length > 0 ? selectedRows : rowData;
// 格式化数据
const exportData = allData.map((row) => {
const formattedRow: Record<string, any> = {};
// 基础字段
formattedRow["姓名"] = row.showname || "";
formattedRow["所属部门"] = row.department?.name || "";
// 动态字段
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
fieldsList.forEach((field: any) => {
const fieldValue = row.fieldValues?.find(
(fv: any) => fv.fieldId === field.id
)?.value;
let displayValue = fieldValue;
// 根据字段类型处理值
if (field.type === "cascader" && fieldValue) {
displayValue = getAreaName(fieldValue.split("/"));
} else if (field.type === "date" && fieldValue) {
displayValue = new Date(fieldValue).toLocaleDateString();
} else if (field.type === "textarea" && fieldValue) {
displayValue = fieldValue.replace(/,/g, "\n");
}
formattedRow[field.label || field.name] = displayValue || "";
});
return formattedRow;
});
// 生成工作表
const ws = utils.json_to_sheet(exportData);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "员工数据");
// 生成文件名
const finalFileName = fileName || defaultFileName;
writeFile(wb, `${finalFileName}.xlsx`);
} catch (error) {
console.error("导出失败:", error);
message.error("导出失败,请稍后重试");
}
}, [fileName, defaultFileName, rowData, selectedRows, fields.data, gridApi]);
// 获取部门数据
const { data: departmentsData } = api.department.findMany.useQuery({
where: { deletedAt: null },
select: { id: true, name: true },
});
// 处理导入数据
const createMany = api.staff.create.useMutation({
onSuccess: () => {
message.success("员工数据导入成功");
// 刷新数据
api.staff.findMany.useQuery();
setImportVisible(false);
},
onError: (error) => {
message.error(`导入失败: ${error.message}`);
},
});
// 处理Excel导入数据
const handleImportData = useCallback(
(excelData: any[]) => {
if (excelData.length === 0) {
message.warning("没有可导入的数据");
return;
}
try {
// 确保字段数据有效提取所有有效的字段ID
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
console.log(
"可用字段列表:",
fieldsList.map((f) => ({ id: f.id, name: f.name, label: f.label }))
);
// 将Excel数据转换为API需要的格式
const staffImportData = excelData.map((row, rowIndex) => {
console.log(`正在处理第${rowIndex + 1}行数据:`, row);
const staff: any = {
// 设置必要的字段
showname: row["姓名"] ? String(row["姓名"]) : "未命名",
// 避免使用自动生成的字段
fieldValues: [],
};
// 处理部门关联
if (row["所属部门"] && departmentsData) {
const deptName = row["所属部门"];
const matchedDept = departmentsData.find(
(dept) => dept.name === deptName
);
if (matchedDept) {
staff.department = {
connect: { id: matchedDept.id },
};
} else {
console.warn(`未找到匹配的部门: ${deptName}`);
}
}
// 我们不在这里处理自定义字段,而是在员工创建后单独处理
console.log(`准备创建员工: ${staff.showname}`);
return staff;
});
// 逐条导入数据
if (staffImportData.length > 0) {
staffImportData.forEach((staffData, index) => {
createMany.mutate(
{ data: staffData },
{
onSuccess: (data) => {
console.log(`员工创建成功:`, data);
message.success(`成功导入第${index + 1}条基础数据`);
// 员工创建成功后,再单独处理自定义字段值
// 由于外键约束问题,我们暂时跳过字段值的创建
// 后续可以添加专门的字段值导入功能
},
onError: (error) => {
message.error(
`导入第${index + 1}条数据失败: ${error.message}`
);
console.error(`导入失败的详细数据:`, staffData);
},
}
);
});
message.info(`正在导入${staffImportData.length}条员工数据...`);
}
} catch (error) {
console.error("处理导入数据失败:", error);
message.error("数据格式错误,导入失败");
}
},
[fields.data, createMany, departmentsData]
);
return (
<>
<Button className='mr-2' danger onClick={handleDelete} style={{ marginBottom: '10px' }}></Button>
<Button type="primary" onClick={handleEdit} style={{ marginBottom: '10px' }}></Button>
<div className="ag-theme-alpine" style={{ height: 500, width: '100%', padding: '20px' }}>
<h1 className="text-2xl mb-4"></h1>
<div className="flex justify-between items-center mb-4">
<div className="space-x-2">
<Button
className="bg-red-500 hover:bg-red-600 border-red-500 text-white rounded-md px-4 py-2"
onClick={handleDelete}
>
</Button>
<Button
className="bg-blue-500 hover:bg-blue-600 border-blue-500 text-white rounded-md px-4 py-2"
onClick={handleEdit}
>
</Button>
<Button
onClick={() => setImportVisible(true)}
className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600"
>
Excel
</Button>
<Button
onClick={handleExportTemplate}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
</Button>
<Button
onClick={handleConfirm}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
</Button>
</div>
<Button
className="bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700 rounded-md px-4 py-2"
onClick={() => {}}
>
</Button>
</div>
<div className="bg-white rstaffDataounded-lg shadow-md p-6 mb-4">
<h1 className="text-2xl font-bold text-gray-800 mb-4"></h1>
<div className="ag-theme-alpine w-full min-h-[480px] h-auto overflow-visible">
<AgGridReact
modules={[SetFilterModule]}
rowData={rowData}
columnDefs={columnDefs}
pagination={true}
paginationPageSize={10}
onSelectionChanged={e => setSelectedRows(e.api.getSelectedRows())}
paginationPageSizeSelector={[10, 20, 50, 100]}
onSelectionChanged={(e) => setSelectedRows(e.api.getSelectedRows())}
rowSelection="multiple"
className="rounded border border-gray-200"
headerHeight={40}
rowHeight={40}
domLayout="autoHeight"
onGridReady={(params) => setGridApi(params.api)}
/>
</div>
<Button onClick={() => {
console.log('字段配置:', fields.data);
console.log('员工数据:', staffData);
}}></Button>
</div>
{/* 编辑弹窗 */}
<Modal
title="编辑员工信息"
open={isEditModalVisible}
onCancel={() => setIsEditModalVisible(false)}
footer={null}
width={1000}
destroyOnClose={true}
>
{currentEditStaff && (
<StaffInfoWrite
staffId={currentEditStaff.id}
initialData={currentEditStaff}
fieldValues={currentEditStaff.fieldValues}
onComplete={handleEditComplete}
setIsEditModalVisible={setIsEditModalVisible}
/>
)}
</Modal>
{/* 导出文件名对话框 */}
<Modal
title="输入文件名"
open={fileNameVisible}
onOk={handleFileNameConfirm}
onCancel={() => setFileNameVisible(false)}
okText="导出"
cancelText="取消"
>
<Input
placeholder={`默认名称: ${defaultFileName}`}
value={fileName}
onChange={(e) => setFileName(e.target.value)}
/>
</Modal>
{/* 导入对话框 */}
<Modal
title="导入员工数据"
open={importVisible}
onCancel={() => setImportVisible(false)}
footer={null}
>
<Upload
accept=".xlsx,.xls"
beforeUpload={(file) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const wb = read(e.target?.result);
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
if (data.length === 0) {
message.warning("Excel文件中没有数据");
return;
}
message.info(`读取到${data.length}条数据,正在处理...`);
handleImportData(data);
} catch (error) {
console.error("解析Excel文件失败:", error);
message.error("Excel文件格式错误请确保使用正确的模板");
}
};
reader.readAsArrayBuffer(file);
return false;
}}
>
<Button icon={<UploadOutlined />}>Excel文件</Button>
</Upload>
<div className="mt-4 text-gray-500 text-sm">
使
</div>
</Modal>
</>
);
}

View File

0
apps/web/src/app/main/staffinfo_write/area-options.ts Normal file → Executable file
View File

0
apps/web/src/app/main/staffinfo_write/infoCard.tsx Normal file → Executable file
View File

View File

@ -5,11 +5,20 @@ import { areaOptions } from './area-options';
import InfoCard from './infoCard';
import DepartmentSelect from "@web/src/components/models/department/department-select";
import { baseFields } from "@web/src/app/admin/staffinfo-manage/defaultFields";
const StaffInfoWrite = () => {
import dayjs from 'dayjs'; // 导入 dayjs
interface StaffInformationProps {
staffId?: string; // 可选,编辑模式时提供
onComplete?: () => void; // 可选,完成时的回调函数
initialData?: any; // 可选,编辑模式时提供的初始数据
fieldValues?: any[]; // 可选,编辑模式时提供的字段值数组
setIsEditModalVisible?: (visible: boolean) => void; // 可选,编辑模式时提供的回调函数
}
const StaffInfoWrite = ({ staffId, onComplete, initialData, fieldValues, setIsEditModalVisible }: StaffInformationProps) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
// 修改使用的hook方法
const { create, setCustomFieldValue, useCustomFields } = useStaff();
const { create, update, setCustomFieldValue, useCustomFields } = useStaff();
const { data: fields, isLoading: fieldsLoading } = useCustomFields();
const [infoCards, setInfoCards] = useState<any[]>([]);
@ -19,7 +28,10 @@ const StaffInfoWrite = () => {
// 添加状态来跟踪每个文本区域的高度
const [textAreaHeights, setTextAreaHeights] = useState<Record<string, number>>({});
const { data: staffData } = api.staff.findUnique.useQuery(
{ where: { id: staffId } },
{ enabled: !!staffId } // 只在 staffId 存在时执行查询
);
const handleAdd = (content: string[]) => {
// 将数组内容展开为独立对象
const newItems = content.map(text => ({ content: text }));
@ -29,26 +41,51 @@ const StaffInfoWrite = () => {
]);
}
// 在组件中添加监听字段变化
// 在组件挂载或依赖项变化时填充表单数据
useEffect(() => {
// 设置默认值
form.setFieldsValue({
hasTrain: false,
hasCert: false
});
// 使用 Form 的 onValuesChange 在外部监听
const fieldChangeHandler = () => {
const values = form.getFieldsValue(['hasTrain', 'hasCert']);
setHasTrain(!!values.hasTrain);
setHasCert(!!values.hasCert);
if (initialData && fieldValues && fields) {
// 设置基础字段
const formValues: any = {
showname: initialData.showname,
department: initialData.deptId
};
// 初始化时执行一次
fieldChangeHandler();
// 设置自定义字段值
if (Array.isArray(fieldValues)) {
fieldValues.forEach(fv => {
// 添加类型断言确保 fields 是数组
const field = Array.isArray(fields) ?
fields.find((f: any) => f.id === fv.fieldId) : undefined;
if (field) {
let value = fv.value;
// 不需要返回取消订阅,因为我们不再使用 subscribe
}, [form]);
// 根据字段类型转换值的格式
if (field.type === 'cascader' && value) {
value = value.split('/');
} else if (field.type === 'textarea' && value) {
value = value.split(',');
} else if (field.type === 'date' && value) {
// 使用 dayjs 而不是 new Date 来处理日期
value = value ? dayjs(value) : null;
}
formValues[field.name] = value;
// 设置培训和鉴定状态
if (field.name === 'hasTrain') {
setHasTrain(value === '是');
}
if (field.name === 'hasCert') {
setHasCert(value === '是');
}
}
});
}
// 设置表单的值
form.setFieldsValue(formValues);
}
}, [initialData, fieldValues, fields, form]);
// 按分组组织字段
const fieldGroups = useMemo(() => {
@ -80,8 +117,8 @@ const StaffInfoWrite = () => {
return (
<Radio.Group
options={[
{ label: '是', value: true },
{ label: '否', value: false }
{ label: '是', value:'是' },
{ label: '否', value:'否' }
]}
onChange={(e) => {
setHasTrain(e.target.value);
@ -96,8 +133,8 @@ const StaffInfoWrite = () => {
return (
<Radio.Group
options={[
{ label: '是', value: true },
{ label: '否', value: false }
{ label: '是', value: '是' },
{ label: '否', value: '否' }
]}
onChange={(e) => {
setHasCert(e.target.value);
@ -169,7 +206,6 @@ const StaffInfoWrite = () => {
}
};
const onFinish = async (e, values: any) => {
// values.preventDefault();
e.preventDefault()
console.log(values)
try {
@ -179,36 +215,51 @@ const StaffInfoWrite = () => {
message.error("姓名不能为空");
return;
}
// 创建基础员工记录
let staff;
if (staffId) {
// 编辑模式 - 更新现有员工
console.log('准备更新用户,数据:', { id: staffId, showname: values.showname });
try {
staff = await update.mutateAsync({
where: { id: staffId },
data: {
showname: values.showname,
deptId: values.department || null,
}
});
console.log('更新员工记录:', staff);
} catch (error) {
console.error('更新员工基本信息失败:', error);
message.error("更新失败,请重试");
setLoading(false);
return; // 如果基本信息更新失败,提前返回不继续处理
}
} else {
// 创建模式 - 创建新员工
console.log('准备创建用户,数据:', { showname: values.showname });
const staff = await create.mutateAsync({
try {
staff = await create.mutateAsync({
data: {
showname: values.showname,
deptId: values.department ? values.department : null,
}
});
console.log('创建员工记录:', staff);
// 创建系统日志记录
await api.systemLog.create.mutateAsync({
level: "info",
module: "人员管理",
action: "创建用户",
targetId: staff.id,
targetName: staff.username,
message: `[${new Date().toLocaleString()}] 用户 ${staff.username} 的人员信息已成功添加`,
details: {
fields: validEntries.map(({ field, value }) => ({
name: field.label,
value
}))
},
status: "success",
departmentId: staff.deptId // 用户所属部门
});
} catch (error) {
console.error('创建员工记录失败:', error);
message.error("提交失败,请重试");
setLoading(false);
return; // 如果创建失败,提前返回不继续处理
}
}
// 只有在上面的操作成功后才继续处理自定义字段
try {
// 过滤有效字段并转换值
const validEntries = Object.entries(values)
.filter(([key, value]) =>
key !== 'showname' && // 新增排除 showname
key !== 'showname' &&
key !== 'username' &&
key !== 'department' &&
value !== undefined &&
@ -221,12 +272,12 @@ const StaffInfoWrite = () => {
// 处理特殊字段类型
let processedValue = value;
if (field?.type === 'date') { //日期类型
processedValue = (value as Date)?.toISOString();
} else if (field?.type === 'cascader' && Array.isArray(value)) { //级联选择器
if (field?.type === 'date') {
processedValue = (value as any)?.format?.('YYYY-MM-DD') || value;
} else if (field?.type === 'cascader' && Array.isArray(value)) {
processedValue = value?.join('/');
}else if(field?.type === 'textarea'){ //多行文本
processedValue = (value as string[])?.join(',');
} else if(field?.type === 'textarea'){
processedValue = Array.isArray(value) ? value.join(',') : value;
}
return { field, value: processedValue };
@ -243,87 +294,28 @@ const StaffInfoWrite = () => {
})
)
);
console.log('自定义字段提交成功', staff.username);
// 记录系统日志 - 用户创建成功
const timestamp = new Date().toLocaleString();
const logs = [];
// 记录用户创建
logs.push(`${timestamp} - 用户创建成功:${staff.username}`);
// 记录人员信息添加
logs.push(`[${timestamp}] 用户 ${staff.username} 的人员信息已成功添加`);
// 记录每个字段的详细信息
validEntries.forEach(({ field, value }) => {
if (field && field.label && value) {
logs.push(`[${timestamp}] 提交的数据: ${field.label}=${value}`);
}
});
// 根据字段分组记录
const fieldsByGroup = validEntries.reduce((groups, { field, value }) => {
if (field && field.group && value) {
if (!groups[field.group]) {
groups[field.group] = [];
}
groups[field.group].push({ field, value });
}
return groups;
}, {});
// 为每个分组记录信息
Object.entries(fieldsByGroup).forEach(([groupName, fields]) => {
const groupValues = (fields as any[]).map(f => `${f.field.label}=${f.value}`).join(', ');
logs.push(`[${timestamp}] ${staff.username}${groupName}${groupValues || '无'}`);
});
// 只有当所有操作都成功时才显示成功消息
message.success(staffId ? "信息更新成功" : "信息提交成功");
// 获取现有日志
let currentLogs = [];
try {
const storedLogs = localStorage.getItem('systemLogs');
currentLogs = storedLogs ? JSON.parse(storedLogs) : [];
} catch (error) {
console.error('读取系统日志失败', error);
// 关闭编辑模态窗口(如果在编辑模式)
if (staffId && setIsEditModalVisible) {
setIsEditModalVisible(false);
}
// 添加新日志(倒序添加,最新的在最前面)
const updatedLogs = [...logs.reverse(), ...currentLogs];
// 保存到 localStorage
localStorage.setItem('systemLogs', JSON.stringify(updatedLogs));
// 如果有全局变量,也更新它
if (typeof window !== 'undefined') {
(window as any).globalLogs = updatedLogs;
}
message.success("信息提交成功");
// 如果有回调函数,调用它
if (onComplete) {
onComplete();
} else if (!staffId) {
// 如果是新建模式,重置表单
form.resetFields();
}
} catch (error) {
console.error('设置自定义字段值失败:', error);
}
} catch (error) {
console.error('提交出错:', error);
// 记录错误日志
const timestamp = new Date().toLocaleString();
const logMessage = `${timestamp} - 创建用户失败:${values.username || '未知用户'}, 错误: ${error.message || '未知错误'}`;
// 获取现有日志
let currentLogs = [];
try {
const storedLogs = localStorage.getItem('systemLogs');
currentLogs = storedLogs ? JSON.parse(storedLogs) : [];
} catch (err) {
console.error('读取系统日志失败', err);
}
// 添加新日志
const updatedLogs = [logMessage, ...currentLogs];
// 保存到 localStorage
localStorage.setItem('systemLogs', JSON.stringify(updatedLogs));
// 如果有全局变量,也更新它
if (typeof window !== 'undefined') {
(window as any).globalLogs = updatedLogs;
}
message.error("提交失败,请重试");
message.error(staffId ? "更新失败,请重试" : "提交失败,请重试");
} finally {
setLoading(false);
}
@ -335,7 +327,7 @@ const StaffInfoWrite = () => {
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6"></h1>
<h1 className="text-2xl font-bold mb-6"></h1>
<Form
form={form}

12
apps/web/src/app/main/systemlog/SystemLogPage.tsx Normal file → Executable file
View File

@ -4,7 +4,10 @@ import React, { useState, useEffect } from 'react';
// 创建一个全局变量来存储日志
let globalLogs: string[] = [];
const clearLogs = () => {
localStorage.removeItem('systemLogs');
setLogs([]);
};
// 添加日志的函数
export const addLog = (log: string) => {
const timestamp = new Date().toLocaleString();
@ -29,6 +32,9 @@ const SystemLogPage = () => {
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6"></h1>
<button onClick={clearLogs} className="mb-4 bg-red-500 text-white p-2 rounded">
</button>
<div className="bg-white p-6 rounded-lg shadow">
{logs.length === 0 ? (
<p className="text-gray-500"></p>
@ -47,3 +53,7 @@ const SystemLogPage = () => {
};
export default SystemLogPage;
function setLogs(arg0: undefined[]) {
throw new Error('Function not implemented.');
}

View File

View File

@ -1,9 +1,6 @@
import { Outlet } from "react-router-dom";
import { Layout } from "antd";
import { adminRoute } from "@web/src/routes/admin-route";
import AdminSidebar from "./AdminSidebar";
const { Content } = Layout;
export default function AdminLayout() {

View File

View File

View File

View File

View File

View File

View File

0
apps/web/src/data/area-options.ts Normal file → Executable file
View File

View File

@ -26,7 +26,6 @@ interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
breadcrumb?: string;
}
export interface CustomNonIndexRouteObject extends NonIndexRouteObject {
name?: string;
children?: CustomRouteObject[];
@ -70,12 +69,10 @@ export const routes: CustomRouteObject[] = [
{
path: "/admin",
element: <AdminLayout></AdminLayout>,
children:adminRoute.children
children: adminRoute.children,
},
],
},
],
},
{
@ -83,6 +80,12 @@ export const routes: CustomRouteObject[] = [
breadcrumb: "登录",
element: <LoginPage></LoginPage>,
},
{
index: true,
path: "/",
element: <DeptSettingPage></DeptSettingPage>,
errorElement: <ErrorPage />,
},
];
export const router = createBrowserRouter(routes);

View File

@ -100,7 +100,7 @@ server {
# 仅供内部使用
internal;
# 代理到认证服务
proxy_pass http://192.168.252.77:3000/auth/file;
proxy_pass http://192.168.252.77:3001/auth/file;
# 请求优化:不传递请求体
proxy_pass_request_body off;

161
package-lock.json generated Normal file
View File

@ -0,0 +1,161 @@
{
"name": "nice-stack",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nice-stack",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2"
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/echarts-for-react": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz",
"integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
},
"peerDependencies": {
"echarts": "^3.0.0 || ^4.0.0 || ^5.0.0",
"react": "^15.0.0 || >=16.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT",
"peer": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/react": {
"version": "18.2.0",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/size-sensor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz",
"integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw=="
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
},
"dependencies": {
"echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"requires": {
"tslib": "2.3.0",
"zrender": "5.6.1"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
},
"echarts-for-react": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz",
"integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==",
"requires": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
}
},
"fast-deep-equal": {
"version": "3.1.3"
},
"js-tokens": {
"version": "4.0.0",
"peer": true
},
"loose-envify": {
"version": "1.4.0",
"peer": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"react": {
"version": "18.2.0",
"peer": true,
"requires": {
"loose-envify": "^1.1.0"
}
},
"size-sensor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz",
"integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw=="
},
"zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"requires": {
"tslib": "2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
}
}
}

View File

@ -11,5 +11,9 @@
},
"keywords": [],
"author": "insiinc",
"license": "ISC"
"license": "ISC",
"dependencies": {
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2"
}
}

0
packages/client/src/api/hooks/useTrainSituation.ts Normal file → Executable file
View File

View File

@ -466,7 +466,8 @@ model Staff {
enabled Boolean? @default(true)
officerId String? @map("officer_id")
phoneNumber String? @map("phone_number")
age Int?@map("age")
sex String?@map("sex")
// 部门关系
domainId String? @map("domain_id")
deptId String? @map("dept_id")
@ -567,7 +568,7 @@ model SystemLog {
// 关联部门
departmentId String? @map("department_id")
department Department? @relation(fields: [departmentId], references: [id])
message String @map("message") // 完整的日志文本内容
message String? @map("message") // 完整的日志文本内容
// 优化索引
@@index([timestamp])
@@index([level])

View File

@ -154,8 +154,6 @@ export const trainSituationDetailSelect: Prisma.TrainSituationSelect = {
name: true,
},
},
age: true,
sex: true,
absent: true,
},
},
@ -173,8 +171,6 @@ export const staffDetailSelect: Prisma.StaffSelect = {
username: true,
deptId: true,
avatar: true,
age: true,
sex: true,
absent: true,
trainSituations:{
select:{

0
packages/utils/src/safePrismaQuery.ts Normal file → Executable file
View File

File diff suppressed because it is too large Load Diff