This commit is contained in:
Li1304553726 2025-04-10 20:24:16 +08:00
parent 1a26202e34
commit 9075be6046
16 changed files with 1639 additions and 486 deletions

View File

@ -29,7 +29,6 @@ export class DepartmentService extends BaseTreeService<Prisma.DepartmentDelegate
// 如果未找到部门,返回空数组 // 如果未找到部门,返回空数组
if (!ancestorDepartment) return []; if (!ancestorDepartment) return [];
// 查询同域下以指定部门为祖先的部门血缘关系 // 查询同域下以指定部门为祖先的部门血缘关系
const departmentAncestries = await db.deptAncestry.findMany({ const departmentAncestries = await db.deptAncestry.findMany({
where: { where: {
@ -335,4 +334,214 @@ export class DepartmentService extends BaseTreeService<Prisma.DepartmentDelegate
// 合并并去重父级和自身节点,返回唯一项 // 合并并去重父级和自身节点,返回唯一项
return getUniqueItems([...parents, ...selfItems], 'id'); 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

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

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() { import StatisticCards from "./StatisticCards";
return ( import ChartControls from "./ChartControls";
<div > import DepartmentCharts from "./DepartmentCharts";
import DepartmentTable from "./DepartmentTable";
</div> 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>
);
} }

View File

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

View File

@ -342,6 +342,12 @@ export default function StaffMessage() {
} }
}, [fileName, defaultFileName, rowData, selectedRows, fields.data, gridApi]); }, [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({ const createMany = api.staff.create.useMutation({
onSuccess: () => { onSuccess: () => {
@ -364,81 +370,78 @@ export default function StaffMessage() {
} }
try { try {
// 将Excel数据转换为API需要的格式 // 确保字段数据有效提取所有有效的字段ID
const staffData = excelData.map((row) => {
const staff: any = { fieldValues: [] };
// 处理基础字段
if (row["姓名"]) staff.showname = row["姓名"];
// 处理部门
if (row["所属部门"]) {
// 简单存储部门名称,后续可能需要查询匹配
staff.departmentName = row["所属部门"];
}
// 处理自定义字段
const fieldsList = Array.isArray(fields?.data) ? fields.data : []; const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
fieldsList.forEach((field: any) => { console.log(
let value = row[field.label || field.name]; "可用字段列表:",
fieldsList.map((f) => ({ id: f.id, name: f.name, label: f.label }))
);
// 跳过空值 // 将Excel数据转换为API需要的格式
if (value === undefined || value === "") return; const staffImportData = excelData.map((row, rowIndex) => {
console.log(`正在处理第${rowIndex + 1}行数据:`, row);
// 根据字段类型处理输入值 const staff: any = {
switch (field.type) { // 设置必要的字段
case "cascader": showname: row["姓名"] ? String(row["姓名"]) : "未命名",
// 级联选择器可能需要将显示名称转回代码 // 避免使用自动生成的字段
// 这里简单保留原值,实际可能需要查询转换 fieldValues: [],
break; };
case "date": // 处理部门关联
// 尝试将日期字符串转换为ISO格式 if (row["所属部门"] && departmentsData) {
try { const deptName = row["所属部门"];
const dateObj = new Date(value); const matchedDept = departmentsData.find(
if (!isNaN(dateObj.getTime())) { (dept) => dept.name === deptName
value = dateObj.toISOString(); );
if (matchedDept) {
staff.department = {
connect: { id: matchedDept.id },
};
} else {
console.warn(`未找到匹配的部门: ${deptName}`);
} }
} catch (e) {
console.error(`日期格式转换错误: ${value}`);
}
break;
case "textarea":
// 将换行符替换回逗号进行存储
if (typeof value === "string") {
value = value.replace(/\n/g, ",");
}
break;
// 可以根据需要添加其他字段类型的处理
} }
// 添加到fieldValues数组 // 我们不在这里处理自定义字段,而是在员工创建后单独处理
staff.fieldValues.push({ console.log(`准备创建员工: ${staff.showname}`);
fieldId: field.id,
value: String(value),
});
});
return staff; return staff;
}); });
// 提交数据 // 逐条导入数据
createMany.mutate({ if (staffImportData.length > 0) {
data: staffData[0], // 由于类型限制,这里只能一条一条导入 staffImportData.forEach((staffData, index) => {
}); createMany.mutate(
// 如果有多条数据,需要循环处理 { data: staffData },
for (let i = 1; i < staffData.length; i++) { {
createMany.mutate({ data: staffData[i] }); 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) { } catch (error) {
console.error("处理导入数据失败:", error); console.error("处理导入数据失败:", error);
message.error("数据格式错误,导入失败"); message.error("数据格式错误,导入失败");
} }
}, },
[fields.data, createMany] [fields.data, createMany, departmentsData]
); );
return ( return (
@ -484,7 +487,7 @@ export default function StaffMessage() {
</Button> </Button>
</div> </div>
<div className="bg-white rounded-lg shadow-md p-6 mb-4"> <div className="bg-white rstaffDataounded-lg shadow-md p-6 mb-4">
<h1 className="text-2xl font-bold text-gray-800 mb-4"></h1> <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"> <div className="ag-theme-alpine w-full min-h-[480px] h-auto overflow-visible">

View File

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

View File

@ -52,7 +52,7 @@ export const routes: CustomRouteObject[] = [
children: [ children: [
{ {
index: true, index: true,
element:<Dashboard></Dashboard>, element: <Dashboard></Dashboard>,
}, },
{ {
path: "/staffinformation", path: "/staffinformation",
@ -69,13 +69,10 @@ export const routes: CustomRouteObject[] = [
{ {
path: "/admin", path: "/admin",
element: <AdminLayout></AdminLayout>, element: <AdminLayout></AdminLayout>,
children:adminRoute.children children: adminRoute.children,
}, },
], ],
}, },
], ],
}, },
{ {
@ -86,9 +83,9 @@ export const routes: CustomRouteObject[] = [
{ {
index: true, index: true,
path: "/", path: "/",
element:<DeptSettingPage></DeptSettingPage>, element: <DeptSettingPage></DeptSettingPage>,
errorElement: <ErrorPage />, errorElement: <ErrorPage />,
} },
]; ];
export const router = createBrowserRouter(routes); export const router = createBrowserRouter(routes);

View File

@ -100,7 +100,7 @@ server {
# 仅供内部使用 # 仅供内部使用
internal; 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; 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": [], "keywords": [],
"author": "insiinc", "author": "insiinc",
"license": "ISC" "license": "ISC",
"dependencies": {
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2"
}
} }

View File

@ -6,7 +6,14 @@ settings:
importers: 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: apps/server:
dependencies: dependencies:
@ -7182,6 +7189,25 @@ packages:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: false 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: /ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@ -7771,7 +7797,6 @@ packages:
/fast-deep-equal@3.1.3: /fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-diff@1.3.0: /fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
@ -11561,6 +11586,10 @@ packages:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: true dev: true
/size-sensor@1.0.2:
resolution: {integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==}
dev: false
/slash@3.0.0: /slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -12235,6 +12264,10 @@ packages:
strip-bom: 3.0.0 strip-bom: 3.0.0
dev: true dev: true
/tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
dev: false
/tslib@2.5.3: /tslib@2.5.3:
resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==}
@ -12929,6 +12962,12 @@ packages:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
dev: false 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): /zustand@4.5.6(@types/react@18.2.38)(react@18.2.0):
resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==}
engines: {node: '>=12.7.0'} engines: {node: '>=12.7.0'}