From 9075be60461329020fa0da5088a5899ac05cd3bc Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Thu, 10 Apr 2025 20:24:16 +0800 Subject: [PATCH] add --- .../models/department/department.service.ts | 211 +++++- .../app/main/admin/deptsettingpage/page.tsx | 700 +++++++++--------- apps/web/src/app/main/home/ChartControls.tsx | 66 ++ .../src/app/main/home/DepartmentCharts.tsx | 139 ++++ .../web/src/app/main/home/DepartmentTable.tsx | 92 +++ apps/web/src/app/main/home/StatisticCards.tsx | 97 +++ apps/web/src/app/main/home/char-options.ts | 111 +++ apps/web/src/app/main/home/page.tsx | 216 +++++- apps/web/src/app/main/layout/MainHeader.tsx | 18 +- .../main/staffinfo_show/staffmessage_page.tsx | 125 ++-- .../components/layout/admin/AdminLayout.tsx | 3 - apps/web/src/routes/index.tsx | 133 ++-- config/nginx/conf.d/web.conf | 2 +- package-lock.json | 161 ++++ package.json | 8 +- pnpm-lock.yaml | 43 +- 16 files changed, 1639 insertions(+), 486 deletions(-) create mode 100644 apps/web/src/app/main/home/ChartControls.tsx create mode 100644 apps/web/src/app/main/home/DepartmentCharts.tsx create mode 100644 apps/web/src/app/main/home/DepartmentTable.tsx create mode 100644 apps/web/src/app/main/home/StatisticCards.tsx create mode 100644 apps/web/src/app/main/home/char-options.ts create mode 100644 package-lock.json diff --git a/apps/server/src/models/department/department.service.ts b/apps/server/src/models/department/department.service.ts index 3ec0e6e..66478d3 100755 --- a/apps/server/src/models/department/department.service.ts +++ b/apps/server/src/models/department/department.service.ts @@ -29,7 +29,6 @@ export class DepartmentService extends BaseTreeService 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 { + const result: Record = {}; + + 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), + })); + } } diff --git a/apps/web/src/app/main/admin/deptsettingpage/page.tsx b/apps/web/src/app/main/admin/deptsettingpage/page.tsx index 8575fbb..c0d9f51 100755 --- a/apps/web/src/app/main/admin/deptsettingpage/page.tsx +++ b/apps/web/src/app/main/admin/deptsettingpage/page.tsx @@ -3,356 +3,392 @@ 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(''); - const [uploadedFileName, setUploadedFileName] = useState(''); - const [fileNameMap, setFileNameMap] = useState>({}); - const [uploadedFiles, setUploadedFiles] = useState<{ id: string, name: string }[]>([]); - const [isDragging, setIsDragging] = useState(false); - const [expireTime, setExpireTime] = useState(null); - const dropRef = useRef(null); + const [uploadedFileId, setUploadedFileId] = useState(""); + const [uploadedFileName, setUploadedFileName] = useState(""); + const [fileNameMap, setFileNameMap] = useState>({}); + const [uploadedFiles, setUploadedFiles] = useState< + { id: string; name: string }[] + >([]); + const [isDragging, setIsDragging] = useState(false); + const [expireTime, setExpireTime] = useState(null); + const dropRef = useRef(null); - // 使用您的 useTusUpload hook - const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({ - onSuccess: (result) => { - setUploadedFileId(result.fileId); - setUploadedFileName(result.fileName); - message.success('文件上传成功'); - }, - onError: (error: Error) => { - message.error('上传失败:' + error.message); - } + // 使用您的 useTusUpload hook + const { uploadProgress, isUploading, uploadError, handleFileUpload } = + useTusUpload({ + onSuccess: (result) => { + setUploadedFileId(result.fileId); + setUploadedFileName(result.fileName); + message.success("文件上传成功"); + }, + onError: (error: Error) => { + message.error("上传失败:" + error.message); + }, }); - // 清除已上传文件 - const handleClearFile = () => { - setUploadedFileId(''); - setUploadedFileName(''); - setUploadedFiles([]); - setFileNameMap({}); - }; + // 清除已上传文件 + const handleClearFile = () => { + setUploadedFileId(""); + setUploadedFileName(""); + setUploadedFiles([]); + setFileNameMap({}); + }; - // 处理文件上传 - const handleFileSelect = async (file: File) => { - // 限制:如果已有上传文件,则提示用户 - if (uploadedFiles.length > 0) { - message.warning('只能上传一个文件,请先删除已上传的文件'); - return; - } - - const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识 + // 处理文件上传 + const handleFileSelect = async (file: File) => { + // 限制:如果已有上传文件,则提示用户 + if (uploadedFiles.length > 0) { + message.warning("只能上传一个文件,请先删除已上传的文件"); + return; + } - handleFileUpload( - file, - async (result) => { - setUploadedFileId(result.fileId); - setUploadedFileName(result.fileName); + const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识 - // 添加到已上传文件列表 - setUploadedFiles([{ id: result.fileId, name: file.name }]); + handleFileUpload( + file, + async (result) => { + setUploadedFileId(result.fileId); + setUploadedFileName(result.fileName); - // 在前端保存文件名映射(用于当前会话) - setFileNameMap({ - [result.fileId]: file.name - }); + // 添加到已上传文件列表 + setUploadedFiles([{ id: result.fileId, name: file.name }]); - // 上传成功后保存原始文件名到数据库 - try { - console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId); + // 在前端保存文件名映射(用于当前会话) + setFileNameMap({ + [result.fileId]: file.name, + }); - const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - fileId: result.fileId, - fileName: file.name - }), - }); - - const responseText = await response.text(); - console.log('保存文件名响应:', response.status, responseText); - - if (!response.ok) { - console.error('保存文件名失败:', responseText); - message.warning('文件名保存失败,下载时可能无法显示原始文件名'); - } else { - console.log('文件名保存成功:', file.name); - } - } catch (error) { - console.error('保存文件名请求失败:', error); - message.warning('文件名保存失败,下载时可能无法显示原始文件名'); - } - - message.success('文件上传成功'); - }, - (error) => { - 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('删除文件失败'); - } - }; + console.log( + "正在保存文件名到数据库:", + result.fileName, + "对应文件ID:", + result.fileId + ); - // 拖拽相关处理函数 - const handleDragEnter = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(true); - }, []); - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - }, []); - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(true); - }, []); - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - handleFileSelect(e.dataTransfer.files[0]); - }, []); - - - // 处理分享码生成成功 - const handleShareSuccess = (code: string) => { - message.success('分享码生成成功:' + code); - // 可以在这里添加其他逻辑,比如保存到历史记录 - }; - - // 处理分享码验证成功 - const handleValidSuccess = async (fileId: string, fileName: string) => { - try { - // 构建下载URL(包含文件名参数) - const downloadUrl = `/upload/download/${fileId}?fileName=${encodeURIComponent(fileName)}`; - const response = await fetch(downloadUrl); - if (!response.ok) { - throw new Error('文件下载失败'); + const response = await fetch( + `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fileId: result.fileId, + fileName: file.name, + }), } + ); - // 创建下载链接 - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; + const responseText = await response.text(); + console.log("保存文件名响应:", response.status, responseText); - // 直接使用传入的 fileName - link.download = fileName; - - // 触发下载 - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - - message.success('文件下载开始'); + if (!response.ok) { + console.error("保存文件名失败:", responseText); + message.warning("文件名保存失败,下载时可能无法显示原始文件名"); + } else { + console.log("文件名保存成功:", file.name); + } } catch (error) { - console.error('下载失败:', error); - message.error('文件下载失败'); + console.error("保存文件名请求失败:", error); + message.warning("文件名保存失败,下载时可能无法显示原始文件名"); } - }; - - return ( -
-

文件分享中心

- - - - {/* 文件上传区域 */} -
-

第一步:上传文件

- - {/* 如果没有已上传文件,显示上传区域 */} - {uploadedFiles.length === 0 ? ( -
- -

点击或拖拽文件到此区域进行上传

-

只能上传单个文件

- - { - const file = e.target.files?.[0]; - if (file) { - handleFileSelect(file); - } - }} - disabled={isUploading} - /> - -
- ) : ( -
-
-

- 您已上传文件,请继续下一步生成分享码 -

-
-
- )} - - {/* 已上传文件列表 */} - {uploadedFiles.length > 0 && ( -
- {uploadedFiles.map((file) => ( -
-
-
- -
- {file.name} -
-
- ))} -
- )} - - {isUploading && ( -
- -
- )} - - {uploadError && ( -
- {uploadError} -
- )} -
- - {/* 生成分享码区域 */} - {uploadedFileId && ( -
-

第二步:生成分享码

- - -
- )} -
- - {/* 使用分享码区域 */} - -
-

使用分享码下载文件

- -
-
-
-
+ message.success("文件上传成功"); + }, + (error) => { + message.error("上传失败:" + error.message); + }, + fileKey ); -} \ No newline at end of file + }; + + // 处理多个文件上传 - 已移除 + // 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) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + handleFileSelect(e.dataTransfer.files[0]); + }, []); + + // 处理分享码生成成功 + const handleShareSuccess = (code: string) => { + message.success("分享码生成成功:" + code); + // 可以在这里添加其他逻辑,比如保存到历史记录 + }; + + // 处理分享码验证成功 + const handleValidSuccess = async (fileId: string, fileName: string) => { + try { + // 构建下载URL(包含文件名参数) + const downloadUrl = `/upload/download/${fileId}?fileName=${encodeURIComponent( + fileName + )}`; + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error("文件下载失败"); + } + + // 创建下载链接 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + + // 直接使用传入的 fileName + link.download = fileName; + + // 触发下载 + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + message.success("文件下载开始"); + } catch (error) { + console.error("下载失败:", error); + message.error("文件下载失败"); + } + }; + + return ( +
+

文件分享中心

+ + + + {/* 文件上传区域 */} +
+

第一步:上传文件

+ + {/* 如果没有已上传文件,显示上传区域 */} + {uploadedFiles.length === 0 ? ( +
+ +

点击或拖拽文件到此区域进行上传

+

+ 只能上传单个文件 +

+ + { + const file = e.target.files?.[0]; + if (file) { + handleFileSelect(file); + } + }} + disabled={isUploading} + /> + +
+ ) : ( +
+
+

+ 您已上传文件,请继续下一步生成分享码 +

+
+
+ )} + + {/* 已上传文件列表 */} + {uploadedFiles.length > 0 && ( +
+ {uploadedFiles.map((file) => ( +
+
+
+ + ✓ + +
+ {file.name} +
+
+ ))} +
+ )} + + {isUploading && ( +
+ +
+ )} + + {uploadError && ( +
+ {uploadError} +
+ )} +
+ + {/* 生成分享码区域 */} + {uploadedFileId && ( +
+

第二步:生成分享码

+ + +
+ )} +
+ + {/* 使用分享码区域 */} + +
+

使用分享码下载文件

+ +
+
+
+
+ ); +} diff --git a/apps/web/src/app/main/home/ChartControls.tsx b/apps/web/src/app/main/home/ChartControls.tsx new file mode 100644 index 0000000..f61bb27 --- /dev/null +++ b/apps/web/src/app/main/home/ChartControls.tsx @@ -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 ( + + + +
+ + 图表控制: + setChartView(e.target.value)} + optionType="button" + buttonStyle="solid" + > + 全部 + 饼图 + 条形图 + + + + {selectedDept && ( + + )} + + {selectedDept + ? `已筛选: ${ + deptDistribution.find((d) => d.id === selectedDept) + ?.name || "" + }` + : "点击饼图可筛选部门"} + + +
+
+ +
+ ); +} diff --git a/apps/web/src/app/main/home/DepartmentCharts.tsx b/apps/web/src/app/main/home/DepartmentCharts.tsx new file mode 100644 index 0000000..7c5ad9c --- /dev/null +++ b/apps/web/src/app/main/home/DepartmentCharts.tsx @@ -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) => void; +} + +export default function DepartmentCharts({ + chartView, + filteredDeptData, + pieOptions, + barOptions, + handleDeptSelection, + handleExportChart, +}: DepartmentChartsProps): React.ReactElement { + const pieChartRef = useRef(null); + const barChartRef = useRef(null); + + return ( + + {/* 条形图 */} + {(chartView === "all" || chartView === "bar") && ( + + +
+ + 部门人员分布 - 条形图 +
+ + + } + className="min-h-96" + contentClassName="flex items-center justify-center" + > + {filteredDeptData.length > 0 ? ( + }> + + + ) : ( + + )} +
+ + )} + + {/* 饼图 */} + {(chartView === "all" || chartView === "pie") && ( + + +
+ + 部门人员分布 - 饼图 +
+ + + } + className="min-h-96" + contentClassName="flex items-center justify-center" + > + {filteredDeptData.length > 0 ? ( + }> + + + ) : ( + + )} +
+ + )} +
+ ); +} diff --git a/apps/web/src/app/main/home/DepartmentTable.tsx b/apps/web/src/app/main/home/DepartmentTable.tsx new file mode 100644 index 0000000..400b012 --- /dev/null +++ b/apps/web/src/app/main/home/DepartmentTable.tsx @@ -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) => ( + + ), + }, + { + title: "占比", + dataIndex: "count", + key: "percentage", + render: (count: number) => ( + + {staffs && staffs.length > 0 + ? ((count / staffs.length) * 100).toFixed(1) + "%" + : "0%"} + + ), + }, + ]; + + return ( + + + +
+ + 部门人员分布详情 +
+ + + + + } + className="min-h-80" + > + {filteredDeptData.length > 0 ? ( + `共 ${total} 个部门`, + }} + size="small" + className="mt-2" + /> + ) : ( + + )} + + + + ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/home/StatisticCards.tsx b/apps/web/src/app/main/home/StatisticCards.tsx new file mode 100644 index 0000000..5efb3ba --- /dev/null +++ b/apps/web/src/app/main/home/StatisticCards.tsx @@ -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 ( + + + + 总人员数量 + + + + + } + className="h-32" + > + } + /> + + + + + 部门总数 + + + + + } + className="h-32" + > + } + /> + + + + + 岗位分布 + + + + + } + className="h-32" + > +
+ } + suffix={`种岗位`} + /> + {positionStats.topPosition && ( +
+ 最多人数岗位: {positionStats.topPosition.name} ( + {positionStats.topPosition.count}人) +
+ )} +
+
+ + + ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/home/char-options.ts b/apps/web/src/app/main/home/char-options.ts new file mode 100644 index 0000000..4bbe420 --- /dev/null +++ b/apps/web/src/app/main/home/char-options.ts @@ -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 void> +): EChartsOption => { + const top10Depts = deptData.slice(0, 10); + return { + tooltip: { + trigger: "item", + formatter: "{a}
{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}人", + }, + }, + ], + }; +}; diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index d6c0312..c5b8084 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -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"; + +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([]); + // 选中的部门筛选 + const [selectedDept, setSelectedDept] = useState(null); + // 图表视图类型 + const [chartView, setChartView] = useState("all"); + // 岗位统计状态 + const [positionStats, setPositionStats] = useState({ + total: 0, + distribution: [], + topPosition: null, + vacantPositions: 0, + }); + + // 处理原始数据,提取部门和岗位分布信息 + useEffect(() => { + if (staffs && departments) { + // 计算部门分布 + const deptMap = new Map(); + 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(); + 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) => { + 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; -export default function Dashboard() { return ( -
- 数据看板(待开发) +
+

数据看板

+ {isLoading ? ( +
+ +
+ ) : ( + <> + {/* 统计卡片 */} + + + {/* 图表筛选和控制区 */} + + + {/* 部门分布数据可视化 */} + + + {/* 详细数据表格 */} + + + )}
- ) -} \ No newline at end of file + ); +} diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index 737fe8f..51ddaa1 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -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 ( {/* 顶部Header */}
- +
{/* 主体布局 */} @@ -22,7 +21,6 @@ export default function MainHeader() { - {/* 内容区域 */} @@ -30,4 +28,4 @@ export default function MainHeader() {
); -} \ No newline at end of file +} diff --git a/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx b/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx index 40e9317..cf612f2 100755 --- a/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx +++ b/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx @@ -342,6 +342,12 @@ export default function StaffMessage() { } }, [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: () => { @@ -364,81 +370,78 @@ export default function StaffMessage() { } 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 staffData = excelData.map((row) => { - const staff: any = { fieldValues: [] }; + const staffImportData = excelData.map((row, rowIndex) => { + console.log(`正在处理第${rowIndex + 1}行数据:`, row); - // 处理基础字段 - if (row["姓名"]) staff.showname = row["姓名"]; + const staff: any = { + // 设置必要的字段 + showname: row["姓名"] ? String(row["姓名"]) : "未命名", + // 避免使用自动生成的字段 + fieldValues: [], + }; - // 处理部门 - if (row["所属部门"]) { - // 简单存储部门名称,后续可能需要查询匹配 - staff.departmentName = row["所属部门"]; + // 处理部门关联 + 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}`); + } } - // 处理自定义字段 - const fieldsList = Array.isArray(fields?.data) ? fields.data : []; - fieldsList.forEach((field: any) => { - let value = row[field.label || field.name]; - - // 跳过空值 - if (value === undefined || value === "") return; - - // 根据字段类型处理输入值 - switch (field.type) { - case "cascader": - // 级联选择器可能需要将显示名称转回代码 - // 这里简单保留原值,实际可能需要查询转换 - break; - - case "date": - // 尝试将日期字符串转换为ISO格式 - try { - const dateObj = new Date(value); - if (!isNaN(dateObj.getTime())) { - value = dateObj.toISOString(); - } - } catch (e) { - console.error(`日期格式转换错误: ${value}`); - } - break; - - case "textarea": - // 将换行符替换回逗号进行存储 - if (typeof value === "string") { - value = value.replace(/\n/g, ","); - } - break; - - // 可以根据需要添加其他字段类型的处理 - } - - // 添加到fieldValues数组 - staff.fieldValues.push({ - fieldId: field.id, - value: String(value), - }); - }); + // 我们不在这里处理自定义字段,而是在员工创建后单独处理 + console.log(`准备创建员工: ${staff.showname}`); return staff; }); - // 提交数据 - createMany.mutate({ - data: staffData[0], // 由于类型限制,这里只能一条一条导入 - }); - // 如果有多条数据,需要循环处理 - for (let i = 1; i < staffData.length; i++) { - createMany.mutate({ data: staffData[i] }); + // 逐条导入数据 + 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}条员工数据...`); } - message.info(`正在导入${staffData.length}条员工数据...`); } catch (error) { console.error("处理导入数据失败:", error); message.error("数据格式错误,导入失败"); } }, - [fields.data, createMany] + [fields.data, createMany, departmentsData] ); return ( @@ -484,7 +487,7 @@ export default function StaffMessage() {
-
+

人员总览

diff --git a/apps/web/src/components/layout/admin/AdminLayout.tsx b/apps/web/src/components/layout/admin/AdminLayout.tsx index 7759f57..156fbd9 100755 --- a/apps/web/src/components/layout/admin/AdminLayout.tsx +++ b/apps/web/src/components/layout/admin/AdminLayout.tsx @@ -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() { diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 3a546e5..bd44f16 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -1,9 +1,9 @@ import { - createBrowserRouter, - IndexRouteObject, - Link, - NonIndexRouteObject, - useParams, + createBrowserRouter, + IndexRouteObject, + Link, + NonIndexRouteObject, + useParams, } from "react-router-dom"; import ErrorPage from "../app/error"; import LoginPage from "../app/login"; @@ -19,76 +19,73 @@ import { adminRoute } from "./admin-route"; import AdminLayout from "../components/layout/admin/AdminLayout"; import SystemLogPage from "../app/main/systemlog/SystemLogPage"; interface CustomIndexRouteObject extends IndexRouteObject { - name?: string; - breadcrumb?: string; + name?: string; + breadcrumb?: string; } interface CustomIndexRouteObject extends IndexRouteObject { - name?: string; - breadcrumb?: string; + name?: string; + breadcrumb?: string; } export interface CustomNonIndexRouteObject extends NonIndexRouteObject { - name?: string; - children?: CustomRouteObject[]; - breadcrumb?: string; - handle?: { - crumb: (data?: any) => void; - }; + name?: string; + children?: CustomRouteObject[]; + breadcrumb?: string; + handle?: { + crumb: (data?: any) => void; + }; } export type CustomRouteObject = - | CustomIndexRouteObject - | CustomNonIndexRouteObject; + | CustomIndexRouteObject + | CustomNonIndexRouteObject; export const routes: CustomRouteObject[] = [ - { - path: "/", - errorElement: , - handle: { - crumb() { - return 主页; - }, - }, - children: [ - { - element: , - children: [ - { - index: true, - element:, - }, - { - path: "/staffinformation", - element: , - }, - { - path: "/staff", - element: , - }, - { - path: "/systemlog", - element: , - }, - { - path: "/admin", - element: , - children:adminRoute.children - }, - - - ], - }, - - ], - }, - { - path: "/login", - breadcrumb: "登录", - element: , - }, - { - index: true, - path: "/", - element:, - errorElement: , - } + { + path: "/", + errorElement: , + handle: { + crumb() { + return 主页; + }, + }, + children: [ + { + element: , + children: [ + { + index: true, + element: , + }, + { + path: "/staffinformation", + element: , + }, + { + path: "/staff", + element: , + }, + { + path: "/systemlog", + element: , + }, + { + path: "/admin", + element: , + children: adminRoute.children, + }, + ], + }, + ], + }, + { + path: "/login", + breadcrumb: "登录", + element: , + }, + { + index: true, + path: "/", + element: , + errorElement: , + }, ]; export const router = createBrowserRouter(routes); diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf index 2dd9d9e..eb29fb7 100755 --- a/config/nginx/conf.d/web.conf +++ b/config/nginx/conf.d/web.conf @@ -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; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..55a663d --- /dev/null +++ b/package-lock.json @@ -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==" + } + } + } + } +} diff --git a/package.json b/package.json index 8156253..c9b902e 100755 --- a/package.json +++ b/package.json @@ -11,5 +11,9 @@ }, "keywords": [], "author": "insiinc", - "license": "ISC" -} \ No newline at end of file + "license": "ISC", + "dependencies": { + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ed2708..bc81c32 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,14 @@ settings: importers: - .: {} + .: + dependencies: + echarts: + specifier: ^5.6.0 + version: 5.6.0 + echarts-for-react: + specifier: ^3.0.2 + version: 3.0.2(echarts@5.6.0)(react@18.2.0) apps/server: dependencies: @@ -7182,6 +7189,25 @@ packages: safe-buffer: 5.2.1 dev: false + /echarts-for-react@3.0.2(echarts@5.6.0)(react@18.2.0): + resolution: {integrity: sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==} + peerDependencies: + echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 + react: ^15.0.0 || >=16.0.0 + dependencies: + echarts: 5.6.0 + fast-deep-equal: 3.1.3 + react: 18.2.0 + size-sensor: 1.0.2 + dev: false + + /echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + dev: false + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -7771,7 +7797,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} @@ -11561,6 +11586,10 @@ packages: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true + /size-sensor@1.0.2: + resolution: {integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==} + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -12235,6 +12264,10 @@ packages: strip-bom: 3.0.0 dev: true + /tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + dev: false + /tslib@2.5.3: resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} @@ -12929,6 +12962,12 @@ packages: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false + /zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + dependencies: + tslib: 2.3.0 + dev: false + /zustand@4.5.6(@types/react@18.2.38)(react@18.2.0): resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} engines: {node: '>=12.7.0'}