diff --git a/package.json b/package.json
index 6a14dcc..91d3109 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,10 @@
"version": "1.0.0",
"description": "a react gantt timeline calendar",
"keywords": [
- "npm",
- "template"
+ "react",
+ "gantt",
+ "timeline",
+ "calendar"
],
"homepage": "https://github.com/eternallycyf/ims-gantt-timeline-calendar",
"bugs": {
@@ -66,13 +68,15 @@
]
},
"resolutions": {
- "dumi": "2.2.17"
+ "dumi": "~2.2.17"
},
"dependencies": {
"@babel/runtime": "^7.23.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "zrender": "^5.5.0"
+ "react-use": "^17.5.0",
+ "zrender": "^5.4.4",
+ "zustand": "^4.5.2"
},
"devDependencies": {
"@ant-design/icons": "^5.2.6",
@@ -91,7 +95,7 @@
"concurrently": "^7",
"conventional-changelog-gitmoji-config": "^1",
"cross-env": "^7",
- "dumi": "2.2.17",
+ "dumi": "~2.2.17",
"dumi-theme-antd-style": "^0.31.0",
"eslint": "^8",
"father": "^4",
diff --git a/src/components/Hello/demo/index.tsx b/src/components/Hello/demo/index.tsx
deleted file mode 100644
index eb7974c..0000000
--- a/src/components/Hello/demo/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import { Hello } from 'ims-gantt-timeline-calendar';
-
-export default Hello;
diff --git a/src/components/Hello/index.md b/src/components/Hello/index.md
deleted file mode 100644
index 8b3ff89..0000000
--- a/src/components/Hello/index.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-title: Hello
-description: Hello
-toc: content
-group:
- title: 分组
- order: 0
-demo:
- cols: 2
----
-
-## Hello
-
-
diff --git a/src/components/Hello/index.tsx b/src/components/Hello/index.tsx
deleted file mode 100644
index fecc853..0000000
--- a/src/components/Hello/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-const Hello = () => {
- return 'word';
-};
-
-export default Hello;
diff --git a/src/components/TimeLine/TimeLine.tsx b/src/components/TimeLine/TimeLine.tsx
new file mode 100644
index 0000000..f42de57
--- /dev/null
+++ b/src/components/TimeLine/TimeLine.tsx
@@ -0,0 +1,246 @@
+import { FC, useEffect } from 'react';
+import { useLocation } from 'react-use';
+import * as zrender from 'zrender';
+import { div } from 'zrender/lib/core/vector';
+import './index.less';
+import type { TimeLineProps } from './interface';
+import { useSearchParams, useSearchParamsActions } from './store/useGanttChartStore';
+import { getRandomColor } from './utils';
+import { hachureLines } from './utils/hachure';
+import { isHoliday } from './utils/holidays';
+import { getRealDuration } from './utils/task';
+
+const TimeLine: FC = (props) => {
+ const { tasks = [] } = props;
+ const { refreshSearchParamsStore } = useSearchParamsActions();
+ const {
+ initChartStartX,
+ initChartStartY,
+ timeScaleHeight,
+ milestoneTopHeight,
+ unitWidth,
+ barHeight,
+ barMargin,
+ halfUnitWidth,
+ taskNamePaddingLeft,
+ } = useSearchParams();
+
+ useEffect(() => {
+ refreshSearchParamsStore();
+ }, [location]);
+
+ const handleInitTimeLine = () => {
+ let container = document.getElementById('TimeLine');
+ let zr = zrender.init(container);
+ // margin left to the container
+ const chartStartX = initChartStartX;
+ // margin top to the container
+ const chartStartY = Math.max(initChartStartY, timeScaleHeight + milestoneTopHeight);
+
+ // 1. 拿到画布的宽
+ const canvasWidth = zr.getWidth()!;
+ const canvasHeight = zr.getHeight();
+ // 2. 计算需要画多少格
+ const timeScaleWidth = Math.ceil(canvasWidth / unitWidth);
+
+ // 3. 画时间轴的矩形,设置位置x,y,长宽,给一个背景色填充
+ const timeScale = new zrender.Rect({
+ shape: {
+ x: chartStartX,
+ y: chartStartY - timeScaleHeight,
+ width: timeScaleWidth * unitWidth,
+ height: timeScaleHeight,
+ },
+ style: {
+ fill: 'rgba(255, 0,0, .2)',
+ },
+ });
+ zr.add(timeScale);
+
+ const lastScrollX = 0;
+ const gridStartX = chartStartX;
+ const gridEndX = timeScaleWidth * unitWidth;
+ const gridLineCount = timeScaleWidth + 1;
+ const deltaScrollX = Math.floor(lastScrollX / unitWidth);
+
+ // 3. 遍历要画的线的个数
+ for (let i = 0 + deltaScrollX, count = 0; count < gridLineCount; i++, count++) {
+ const gridX = gridStartX + i * unitWidth;
+ // 4. 画一根线,从(x1, y1) -> (x2, y2)
+ const gridLine = new zrender.Line({
+ shape: {
+ x1: gridX,
+ y1: chartStartY - timeScaleHeight,
+ x2: gridX,
+ y2: chartStartY + (barHeight + barMargin) * tasks.length,
+ },
+ style: {
+ stroke: 'lightgray',
+ },
+ });
+ // 1. 线比格子多1,所以要提前1步退出
+ if (count < gridLineCount - 1) {
+ // MARK: 同一个遍历画「休息日斜线」
+ const now = +new Date('2024-01-01');
+ const currentDate = now + i * 60 * 1000 * 60 * 24;
+ const dateInfo = isHoliday(currentDate);
+ // 是休息日的话画斜线
+ if (dateInfo.isHoliday) {
+ try {
+ // 返回要画的线的开始、结束坐标
+ const lines = hachureLines(
+ [
+ [chartStartX + i * unitWidth, chartStartY],
+ [chartStartX + i * unitWidth + unitWidth, chartStartY],
+ [
+ chartStartX + i * unitWidth + unitWidth,
+ chartStartY + (barHeight + barMargin) * tasks.length,
+ ],
+ [chartStartX + i * unitWidth, chartStartY + (barHeight + barMargin) * tasks.length],
+ ],
+ 10,
+ 45,
+ );
+ // 用zrender画线段,描边上色
+ lines.forEach((line) => {
+ const [x1, y1] = line[0];
+ const [x2, y2] = line[1];
+ const l = new zrender.Line({
+ shape: {
+ x1,
+ y1,
+ x2,
+ y2,
+ },
+ style: {
+ stroke: 'rgba(221, 221, 221, 0.7)',
+ },
+ });
+ zr.add(l);
+ });
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ // 2. 画基本文本,可以直接改成日期名字,这里直接写遍历的index
+ const dateText = new zrender.Text({
+ style: {
+ // text: i,
+ text: dateInfo.dateString,
+ x: gridX,
+ y: chartStartY - timeScaleHeight,
+ },
+ z: 1,
+ });
+ // 3. 为了居中,要算出文本的宽高
+ const { width, height } = dateText.getBoundingRect();
+ // 4. 重新设置日期文本位置,居中
+ dateText.attr({
+ style: {
+ x: gridX - width / 2 + halfUnitWidth,
+ y: chartStartY - timeScaleHeight - height / 2 + timeScaleHeight / 2,
+ },
+ });
+ // 5. 加到zrender实例中
+ zr.add(dateText);
+ }
+
+ zr.add(gridLine);
+ }
+
+ // 1. 遍历tasks数组
+ tasks.forEach(function (task, index) {
+ // 2. 因为有最后一行是空行,没有任务,用来创建新任务,轮空不画
+ if (!task?.name) return;
+ // 3. 计算任务的绘制位置和矩形宽高
+ const x = chartStartX + task.start * unitWidth;
+ const y = chartStartY + (barHeight + barMargin) * index;
+ const width = task.duration * unitWidth;
+ const taskBarRect = {
+ width,
+ height: barHeight,
+ };
+ // 4. 建一个组,设置可以拖拽 (感谢这个属性,后续交互省了很多力气,还可以设置只能垂直或者水平拖动)
+ const group = new zrender.Group({
+ x,
+ y,
+ draggable: true, // Enable draggable for the group
+ // draggable: "horizontal", // Enable draggable for the group
+ });
+ // 5. 创建任务跨度矩形
+ const rect = new zrender.Rect({
+ shape: {
+ x: 0,
+ y: 0,
+ width: width,
+ height: barHeight,
+ r: 6,
+ },
+ style: {
+ fill: task.fillColor,
+ },
+ cursor: 'move',
+ });
+ // 6. 加到组里
+ group.add(rect);
+
+ // Create a text shape for task name
+ const taskName = new zrender.Text({
+ style: {
+ text: task.name,
+ x: taskNamePaddingLeft,
+ y: barHeight / 2 - 12 / 2,
+ textFill: 'white',
+ textAlign: 'left',
+ textVerticalAlign: 'middle',
+ fill: 'white',
+ } as any,
+ cursor: 'move',
+ });
+ group.add(taskName);
+ // Create a text shape for resource assignment
+ const resourceText = new zrender.Text({
+ style: {
+ text: 'Assigned to: ' + task.resource,
+ x: 0 + width + 5,
+ y: barHeight / 2 + 0 - 12 / 2,
+ textFill: 'black',
+ } as any,
+ cursor: 'normal',
+ });
+ group.add(resourceText);
+ const taskDurationText = new zrender.Text({
+ style: {
+ text: `${getRealDuration(task, false)}天`,
+ x: width - taskNamePaddingLeft,
+ y: barHeight / 2 - 12 / 2,
+ textFill: 'white',
+ textAlign: 'left',
+ textVerticalAlign: 'middle',
+ fill: 'white',
+ } as any,
+ cursor: 'move',
+ });
+ const { width: taskDurationTextWidth } = taskDurationText.getBoundingRect();
+ taskDurationText.attr({
+ style: {
+ x: width - taskDurationTextWidth - taskNamePaddingLeft,
+ },
+ });
+ group.add(taskDurationText);
+
+ zr.add(group);
+ });
+ };
+
+ useEffect(handleInitTimeLine, []);
+
+ return (
+
+ );
+};
+
+export default TimeLine;
diff --git a/src/components/TimeLine/demo/index.less b/src/components/TimeLine/demo/index.less
new file mode 100644
index 0000000..dd6f02e
--- /dev/null
+++ b/src/components/TimeLine/demo/index.less
@@ -0,0 +1,81 @@
+#zrender-container,
+#off-screen {
+ outline: 1px solid red;
+ /* margin: 20px 0 0 150px; */
+}
+
+.popup-wrapper {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ background-color: rgba(255, 0, 0, 0.3);
+ /* display: flex; */
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ display: none;
+}
+
+.popup-wrapper .popup-wrapper__body {
+ width: 337px;
+}
+
+.popup-wrapper .popup-wrapper__body .button-wrapper button {
+ display: block;
+}
+
+.popup-wrapper .popup-wrapper__body textarea {
+ display: block;
+ width: 100%;
+}
+
+.popup-wrapper .popup-wrapper__body .button-wrapper {
+ display: flex;
+ justify-content: space-between;
+}
+
+.popup-wrapper .popup-wrapper__body .button-wrapper button {
+ width: 100px;
+ height: 32px;
+ cursor: pointer;
+}
+
+.popup-wrapper .popup-wrapper__body .button-wrapper button.delete:hover {
+ background-color: #f00;
+ outline: none;
+ border-color: #f00;
+ color: #fff;
+}
+
+.popup-wrapper .popup-wrapper__body .button-wrapper button.copy:hover {
+ background-color: #86a5e5;
+ outline: none;
+ border-color: #86a5e5;
+ color: #fff;
+}
+
+.popup-wrapper .popup-wrapper__body .button-wrapper button.modify:hover {
+ background-color: #1f8a6f;
+ outline: none;
+ border-color: #1f8a6f;
+ color: #fff;
+}
+
+#color-picker {
+ display: flex;
+ margin: 5px 0;
+}
+
+#color-picker>div {
+ width: 80px;
+ height: 30px;
+ margin-right: 10px;
+}
+
+#color-picker>div:hover {
+ cursor: pointer;
+ background-color: attr(data-color) !important;
+}
\ No newline at end of file
diff --git a/src/components/TimeLine/demo/index.tsx b/src/components/TimeLine/demo/index.tsx
new file mode 100644
index 0000000..0fb0931
--- /dev/null
+++ b/src/components/TimeLine/demo/index.tsx
@@ -0,0 +1,26 @@
+import { TimeLine, getRandomColor } from 'ims-gantt-timeline-calendar';
+import './index.less';
+
+const tasks = [
+ { name: 'Task 1', start: 0, duration: 3, resource: 'John', fillColor: getRandomColor() },
+ { name: 'Task 2', start: 2, duration: 4, resource: 'Jane', fillColor: getRandomColor() },
+ {
+ name: 'Task 3 long long long',
+ start: 7,
+ duration: 1,
+ resource: 'Bob',
+ fillColor: getRandomColor(),
+ },
+ { name: 'Task 4', start: 8, duration: 2, resource: 'Bose', fillColor: getRandomColor() },
+ { name: 'Task 5', start: 10, duration: 8, resource: 'Uno', fillColor: getRandomColor() },
+ {},
+ // Add more tasks as needed
+];
+
+export default () => {
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/src/components/TimeLine/hooks/index.ts b/src/components/TimeLine/hooks/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/TimeLine/index.less b/src/components/TimeLine/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/TimeLine/index.md b/src/components/TimeLine/index.md
new file mode 100644
index 0000000..35ec35e
--- /dev/null
+++ b/src/components/TimeLine/index.md
@@ -0,0 +1,13 @@
+---
+title: TimeLine
+toc: content
+group:
+ title: 分组
+ order: 0
+demo:
+ cols: 2
+---
+
+## TimeLine
+
+
diff --git a/src/components/TimeLine/index.ts b/src/components/TimeLine/index.ts
new file mode 100644
index 0000000..5fe93dd
--- /dev/null
+++ b/src/components/TimeLine/index.ts
@@ -0,0 +1,2 @@
+import TimeLine from './TimeLine';
+export default TimeLine;
diff --git a/src/components/TimeLine/interface.ts b/src/components/TimeLine/interface.ts
new file mode 100644
index 0000000..5c5906a
--- /dev/null
+++ b/src/components/TimeLine/interface.ts
@@ -0,0 +1,3 @@
+export interface TimeLineProps {
+ tasks?: any[];
+}
diff --git a/src/components/TimeLine/store/useGanttChartStore.ts b/src/components/TimeLine/store/useGanttChartStore.ts
new file mode 100644
index 0000000..e36fba3
--- /dev/null
+++ b/src/components/TimeLine/store/useGanttChartStore.ts
@@ -0,0 +1,106 @@
+import { create } from 'zustand';
+import { StorageEnum } from '../type/enum';
+import { GetUrlParms } from '../utils';
+import { getItem, removeItem, setItem } from '../utils/storage';
+
+export interface SearchParamsType {
+ currentGroup: any;
+ debug: boolean;
+ taskOwner: string;
+ unitWidth: number;
+ halfUnitWidth: number;
+ taskNamePaddingLeft: number;
+ initChartStartX: number;
+ initChartStartY: number;
+ timeScaleHeight: number;
+ milestoneTopHeight: number;
+ barHeight: number;
+ barMargin: number;
+ scrollSpeed: number;
+ includeHoliday: boolean;
+ useLocal: boolean;
+ useRemote: boolean;
+ view: string;
+ mockTaskSize: number;
+ todayOffset: number;
+ initLastScrollX: number;
+ filter: React.CSSProperties['color'];
+ showFilter: boolean;
+ arrowSize: number;
+ showArrow: boolean;
+}
+
+interface SearchParamsStore {
+ searchParams: SearchParamsType;
+ actions: {
+ refreshSearchParamsStore: () => void;
+ setSearchParamsStore: (settings: Partial) => void;
+ clearSearchParamsStore: () => void;
+ };
+}
+
+const useSearchParamsStore = create((set, get) => {
+ const unitWidth = 160;
+ const halfUnitWidth = 160 / 2;
+ const barHeight = 30;
+ const todayOffset = Math.floor((+new Date() - +new Date('2024-01-01')) / (60 * 60 * 24 * 1000));
+
+ return {
+ searchParams: getItem(StorageEnum.searchParams) || {
+ currentGroup: null,
+ debug: false,
+ taskOwner: 'alexq',
+ unitWidth,
+ halfUnitWidth,
+ taskNamePaddingLeft: 15,
+ initChartStartX: 1,
+ initChartStartY: 50,
+ timeScaleHeight: 20,
+ milestoneTopHeight: 20,
+ barHeight: 30,
+ barMargin: 1,
+ scrollSpeed: 35,
+ includeHoliday: false,
+ useLocal: false,
+ useRemote: false,
+ view: '',
+ mockTaskSize: 0,
+ todayOffset: todayOffset,
+ initLastScrollX: (todayOffset - 1) * unitWidth,
+ filter: undefined,
+ showFilter: false,
+ arrowSize: (barHeight / 3) * 2,
+ showArrow: true,
+ },
+ actions: {
+ refreshSearchParamsStore: () => {
+ const params = GetUrlParms();
+ const { [StorageEnum.searchParams]: defaultSearchParams } = get();
+ const newParams = {
+ ...defaultSearchParams,
+ ...params,
+ halfUnitWidth: (params?.unitWidth || unitWidth) / 2,
+ barMargin: params?.debug ? 10 : params?.barMargin ?? 1,
+ view: params?.view ?? '',
+ mockTaskSize:
+ !params?.useRemote && !params?.useLocal && params?.mockTaskSize
+ ? Number(params.mockTaskSize)
+ : 0,
+ initLastScrollX: (params?.todayOffset - 1) * (params?.unitWidth ?? unitWidth),
+ };
+ set({ [StorageEnum.searchParams]: newParams });
+ setItem(StorageEnum.searchParams, newParams);
+ },
+ setSearchParamsStore: (searchParams) => {
+ const { [StorageEnum.searchParams]: defaultSearchParams } = get();
+ const newDefaultSearchParams = { ...defaultSearchParams, ...searchParams };
+ set({ [StorageEnum.searchParams]: newDefaultSearchParams });
+ setItem(StorageEnum.searchParams, newDefaultSearchParams);
+ },
+ clearSearchParamsStore: () => removeItem(StorageEnum.searchParams),
+ },
+ };
+});
+
+export const useSearchParams = () => useSearchParamsStore((state) => state.searchParams);
+export const useSearchParamsActions = () => useSearchParamsStore((state) => state.actions);
diff --git a/src/components/TimeLine/type/enum.ts b/src/components/TimeLine/type/enum.ts
new file mode 100644
index 0000000..e972db6
--- /dev/null
+++ b/src/components/TimeLine/type/enum.ts
@@ -0,0 +1,3 @@
+export enum StorageEnum {
+ searchParams = 'searchParams',
+}
diff --git a/src/components/TimeLine/type/utils.ts b/src/components/TimeLine/type/utils.ts
new file mode 100644
index 0000000..8f008d1
--- /dev/null
+++ b/src/components/TimeLine/type/utils.ts
@@ -0,0 +1,15 @@
+// // 获取枚举的 value
+// type EnumValues = `${StorageEnum}`
+
+// // 获取枚举的 key
+// type EnumKeys = keyof typeof StorageEnum
+
+/** https://github.com/Microsoft/TypeScript/issues/29729 */
+export type LiteralUnion = T | (string & {});
+
+export type ValuesOf = T[keyof T];
+
+export type AddStringPrefix<
+ Str extends string,
+ String extends string = '',
+> = Str extends `${infer Rest}` ? `${String}${Rest}` : Str;
diff --git a/src/components/TimeLine/utils/hachure.ts b/src/components/TimeLine/utils/hachure.ts
new file mode 100644
index 0000000..86fc9ab
--- /dev/null
+++ b/src/components/TimeLine/utils/hachure.ts
@@ -0,0 +1,158 @@
+function rotatePoints(points: any[], center: any[], degrees: any) {
+ if (points && points.length) {
+ const [cx, cy] = center;
+ const angle = (Math.PI / 180) * degrees;
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+ for (const p of points) {
+ const [x, y] = p;
+ p[0] = (x - cx) * cos - (y - cy) * sin + cx;
+ p[1] = (x - cx) * sin + (y - cy) * cos + cy;
+ }
+ }
+}
+
+function rotateLines(lines: any[], center: any[], degrees: any) {
+ const points: any[] = [];
+ lines.forEach((line: any) => points.push(...line));
+ rotatePoints(points, center, degrees);
+}
+
+function areSamePoints(p1: any[], p2: any[]) {
+ return p1[0] === p2[0] && p1[1] === p2[1];
+}
+
+export function hachureLines(
+ polygons: any[],
+ hachureGap: any,
+ hachureAngle: any,
+ hachureStepOffset = 1,
+) {
+ const angle = hachureAngle;
+ const gap = Math.max(hachureGap, 0.1);
+ const polygonList =
+ polygons[0] && polygons[0][0] && typeof polygons[0][0] === 'number' ? [polygons] : polygons;
+ const rotationCenter = [0, 0];
+ if (angle) {
+ for (const polygon of polygonList) {
+ rotatePoints(polygon, rotationCenter, angle);
+ }
+ }
+ const lines = straightHachureLines(polygonList, gap, hachureStepOffset);
+ if (angle) {
+ for (const polygon of polygonList) {
+ rotatePoints(polygon, rotationCenter, -angle);
+ }
+ rotateLines(lines, rotationCenter, -angle);
+ }
+ return lines;
+}
+function straightHachureLines(polygons: any[], gap: number, hachureStepOffset: number) {
+ const vertexArray: any[] = [];
+ for (const polygon of polygons) {
+ const vertices = [...polygon];
+ if (!areSamePoints(vertices[0], vertices[vertices.length - 1])) {
+ vertices.push([vertices[0][0], vertices[0][1]]);
+ }
+ if (vertices.length > 2) {
+ vertexArray.push(vertices);
+ }
+ }
+ const lines: any[] = [];
+ // eslint-disable-next-line no-param-reassign
+ gap = Math.max(gap, 0.1);
+ // Create sorted edges table
+ const edges: any[] = [];
+ for (const vertices of vertexArray) {
+ for (let i = 0; i < vertices.length - 1; i++) {
+ const p1 = vertices[i];
+ const p2 = vertices[i + 1];
+ if (p1[1] !== p2[1]) {
+ const ymin = Math.min(p1[1], p2[1]);
+ edges.push({
+ ymin,
+ ymax: Math.max(p1[1], p2[1]),
+ x: ymin === p1[1] ? p1[0] : p2[0],
+ islope: (p2[0] - p1[0]) / (p2[1] - p1[1]),
+ });
+ }
+ }
+ }
+ edges.sort((e1, e2) => {
+ if (e1.ymin < e2.ymin) {
+ return -1;
+ }
+ if (e1.ymin > e2.ymin) {
+ return 1;
+ }
+ if (e1.x < e2.x) {
+ return -1;
+ }
+ if (e1.x > e2.x) {
+ return 1;
+ }
+ if (e1.ymax === e2.ymax) {
+ return 0;
+ }
+ return (e1.ymax - e2.ymax) / Math.abs(e1.ymax - e2.ymax);
+ });
+ if (!edges.length) {
+ return lines;
+ }
+ // Start scanning
+ let activeEdges: any[] = [];
+ let y = edges[0].ymin;
+ let iteration = 0;
+ while (activeEdges.length || edges.length) {
+ if (edges.length) {
+ let ix = -1;
+ for (let i = 0; i < edges.length; i++) {
+ if (edges[i].ymin > y) {
+ break;
+ }
+ ix = i;
+ }
+ const removed = edges.splice(0, ix + 1);
+ // eslint-disable-next-line @typescript-eslint/no-loop-func
+ removed.forEach((edge) => {
+ activeEdges.push({ s: y, edge });
+ });
+ }
+ // eslint-disable-next-line @typescript-eslint/no-loop-func
+ activeEdges = activeEdges.filter((ae) => {
+ if (ae.edge.ymax <= y) {
+ return false;
+ }
+ return true;
+ });
+ activeEdges.sort((ae1, ae2) => {
+ if (ae1.edge.x === ae2.edge.x) {
+ return 0;
+ }
+ return (ae1.edge.x - ae2.edge.x) / Math.abs(ae1.edge.x - ae2.edge.x);
+ });
+ // fill between the edges
+ if (hachureStepOffset !== 1 || iteration % gap === 0) {
+ if (activeEdges.length > 1) {
+ for (let i = 0; i < activeEdges.length; i = i + 2) {
+ const nexti = i + 1;
+ if (nexti >= activeEdges.length) {
+ break;
+ }
+ const ce = activeEdges[i].edge;
+ const ne = activeEdges[nexti].edge;
+ lines.push([
+ [Math.round(ce.x), y],
+ [Math.round(ne.x), y],
+ ]);
+ }
+ }
+ }
+ y += hachureStepOffset;
+ activeEdges.forEach((ae) => {
+ ae.edge.x = ae.edge.x + hachureStepOffset * ae.edge.islope;
+ });
+ iteration++;
+ }
+ return lines;
+}
diff --git a/src/components/TimeLine/utils/holidays.ts b/src/components/TimeLine/utils/holidays.ts
new file mode 100644
index 0000000..eb45487
--- /dev/null
+++ b/src/components/TimeLine/utils/holidays.ts
@@ -0,0 +1,275 @@
+const holidays = {
+ '01-01': {
+ holiday: true,
+ name: '元旦',
+ wage: 3,
+ date: '2024-01-01',
+ rest: 1,
+ },
+ '02-04': {
+ holiday: false,
+ name: '春节前补班',
+ wage: 1,
+ after: false,
+ target: '春节',
+ date: '2024-02-04',
+ rest: 34,
+ },
+ '02-10': {
+ holiday: true,
+ name: '初一',
+ wage: 3,
+ date: '2024-02-10',
+ rest: 40,
+ },
+ '02-11': {
+ holiday: true,
+ name: '初二',
+ wage: 3,
+ date: '2024-02-11',
+ },
+ '02-12': {
+ holiday: true,
+ name: '初三',
+ wage: 3,
+ date: '2024-02-12',
+ },
+ '02-13': {
+ holiday: true,
+ name: '初四',
+ wage: 2,
+ date: '2024-02-13',
+ },
+ '02-14': {
+ holiday: true,
+ name: '初五',
+ wage: 2,
+ date: '2024-02-14',
+ },
+ '02-15': {
+ holiday: true,
+ name: '初六',
+ wage: 2,
+ date: '2024-02-15',
+ },
+ '02-16': {
+ holiday: true,
+ name: '初七',
+ wage: 2,
+ date: '2024-02-16',
+ },
+ '02-17': {
+ holiday: true,
+ name: '初八',
+ wage: 2,
+ date: '2024-02-17',
+ },
+ '02-18': {
+ holiday: false,
+ name: '春节后补班',
+ wage: 1,
+ after: true,
+ target: '春节',
+ date: '2024-02-18',
+ },
+ '04-04': {
+ holiday: true,
+ name: '清明节',
+ wage: 3,
+ date: '2024-04-04',
+ rest: 46,
+ },
+ '04-05': {
+ holiday: true,
+ name: '清明节',
+ wage: 2,
+ date: '2024-04-05',
+ },
+ '04-06': {
+ holiday: true,
+ name: '清明节',
+ wage: 2,
+ date: '2024-04-06',
+ },
+ '04-07': {
+ holiday: false,
+ name: '清明节后补班',
+ wage: 1,
+ target: '清明节',
+ after: true,
+ date: '2024-04-07',
+ },
+ '04-28': {
+ holiday: false,
+ name: '劳动节前补班',
+ wage: 1,
+ target: '劳动节',
+ after: false,
+ date: '2024-04-28',
+ },
+ '05-01': {
+ holiday: true,
+ name: '劳动节',
+ wage: 3,
+ date: '2024-05-01',
+ },
+ '05-02': {
+ holiday: true,
+ name: '劳动节',
+ wage: 2,
+ date: '2024-05-02',
+ },
+ '05-03': {
+ holiday: true,
+ name: '劳动节',
+ wage: 3,
+ date: '2024-05-03',
+ },
+ '05-04': {
+ holiday: true,
+ name: '劳动节',
+ wage: 3,
+ date: '2024-05-04',
+ },
+ '05-05': {
+ holiday: true,
+ name: '劳动节',
+ wage: 3,
+ date: '2024-05-05',
+ },
+ '05-11': {
+ holiday: false,
+ name: '劳动节后补班',
+ after: true,
+ wage: 1,
+ target: '劳动节',
+ date: '2024-05-11',
+ },
+ '06-08': {
+ holiday: true,
+ name: '端午节',
+ wage: 2,
+ date: '2024-06-08',
+ },
+ '06-09': {
+ holiday: true,
+ name: '端午节',
+ wage: 2,
+ date: '2024-06-09',
+ },
+ '06-10': {
+ holiday: true,
+ name: '端午节',
+ wage: 3,
+ date: '2024-06-10',
+ },
+ '09-14': {
+ holiday: false,
+ name: '中秋节前补班',
+ after: false,
+ wage: 1,
+ target: '中秋节',
+ date: '2024-09-14',
+ },
+ '09-15': {
+ holiday: true,
+ name: '中秋节',
+ wage: 2,
+ date: '2024-09-15',
+ },
+ '09-16': {
+ holiday: true,
+ name: '中秋节',
+ wage: 2,
+ date: '2024-09-16',
+ },
+ '09-17': {
+ holiday: true,
+ name: '中秋节',
+ wage: 3,
+ date: '2024-09-17',
+ },
+ '09-29': {
+ holiday: false,
+ name: '国庆节前补班',
+ after: false,
+ wage: 1,
+ target: '国庆节',
+ date: '2024-09-29',
+ },
+ '10-01': {
+ holiday: true,
+ name: '国庆节',
+ wage: 3,
+ date: '2024-10-01',
+ },
+ '10-02': {
+ holiday: true,
+ name: '国庆节',
+ wage: 3,
+ date: '2024-10-02',
+ rest: 1,
+ },
+ '10-03': {
+ holiday: true,
+ name: '国庆节',
+ wage: 3,
+ date: '2024-10-03',
+ },
+ '10-04': {
+ holiday: true,
+ name: '国庆节',
+ wage: 2,
+ date: '2024-10-04',
+ },
+ '10-05': {
+ holiday: true,
+ name: '国庆节',
+ wage: 2,
+ date: '2024-10-05',
+ },
+ '10-06': {
+ holiday: true,
+ name: '国庆节',
+ wage: 2,
+ date: '2024-10-06',
+ rest: 1,
+ },
+ '10-07': {
+ holiday: true,
+ name: '国庆节',
+ wage: 2,
+ date: '2024-10-07',
+ rest: 1,
+ },
+ '10-12': {
+ holiday: false,
+ after: true,
+ wage: 1,
+ name: '国庆节后补班',
+ target: '国庆节',
+ date: '2024-10-12',
+ },
+};
+
+export function isHoliday(dateString: any) {
+ const d = new Date(dateString);
+ const month = d.getMonth() + 1 + '';
+ const monthWithPadding = month.padStart(2, '0');
+ const day = d.getDate() + '';
+ const dayWithPadding = day.padStart(2, '0');
+ const date_key = `${monthWithPadding}-${dayWithPadding}`;
+ const isWeekend = [0, 6].indexOf(d.getDay()) != -1;
+ //@ts-ignore
+ if (holidays[date_key]) {
+ return {
+ //@ts-ignore
+ isHoliday: holidays[date_key].holiday,
+ dateString: `${month}-${day}`,
+ };
+ }
+ return {
+ isHoliday: isWeekend,
+ dateString: `${month}-${day}`,
+ };
+}
diff --git a/src/components/TimeLine/utils/index.ts b/src/components/TimeLine/utils/index.ts
new file mode 100644
index 0000000..d9268ca
--- /dev/null
+++ b/src/components/TimeLine/utils/index.ts
@@ -0,0 +1,42 @@
+import type { SearchParamsType } from '../store/useGanttChartStore';
+
+export function getParamsFromSearch(key: SearchParamsType, autoConvert = true) {
+ const params = new URLSearchParams(location.search);
+ return params.get(key as unknown as string) && autoConvert
+ ? Number(params.get(key as unknown as string))
+ : params.get(key as unknown as string);
+}
+
+export function GetUrlParms(): Record {
+ let args: any = new Object();
+ let query = decodeURIComponent(location.search).substring(1);
+ let pairs = query.split('&');
+ for (let i = 0; i < pairs.length; i++) {
+ let pos = pairs[i].indexOf('=');
+ if (pos == -1) continue;
+ let argname = pairs[i].substring(0, pos);
+ let value = pairs[i].substring(pos + 1);
+ args[argname] = unescape(value);
+ }
+ return args;
+}
+
+export function getRandomColor() {
+ // Generate random values for red, green, and blue components
+ const r = Math.floor(Math.random() * 256);
+ const g = Math.floor(Math.random() * 256);
+ const b = Math.floor(Math.random() * 256);
+
+ // Convert values to hexadecimal and format the color
+ const hexColor =
+ '#' +
+ r.toString(16).padStart(2, '0') +
+ g.toString(16).padStart(2, '0') +
+ b.toString(16).padStart(2, '0');
+
+ return hexColor;
+}
+
+export const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
+ navigator.userAgent,
+);
diff --git a/src/components/TimeLine/utils/storage.ts b/src/components/TimeLine/utils/storage.ts
new file mode 100644
index 0000000..847efd7
--- /dev/null
+++ b/src/components/TimeLine/utils/storage.ts
@@ -0,0 +1,26 @@
+import type { StorageEnum } from '../type/enum';
+
+export function getItem(key: StorageEnum): T | null {
+ let value = null;
+ try {
+ const result = window.localStorage.getItem(key);
+ if (result) value = JSON.parse(result);
+ } catch (error) {
+ console.error(error);
+ }
+ return value;
+}
+
+export function getStringItem(key: StorageEnum): string | null {
+ return localStorage.getItem(key);
+}
+
+export function setItem(key: StorageEnum, value: T): void {
+ localStorage.setItem(key, JSON.stringify(value));
+}
+export function removeItem(key: StorageEnum): void {
+ localStorage.removeItem(key);
+}
+export function clearItems() {
+ localStorage.clear();
+}
diff --git a/src/components/TimeLine/utils/task.ts b/src/components/TimeLine/utils/task.ts
new file mode 100644
index 0000000..47acc18
--- /dev/null
+++ b/src/components/TimeLine/utils/task.ts
@@ -0,0 +1,17 @@
+import { isHoliday } from './holidays';
+
+const taskStartDate = +new Date('2024-01-01');
+const taskDayCount = 60 * 60 * 24 * 1000;
+
+// 获取天数
+export function getRealDuration(task: any, includeHoliday: any) {
+ const { start, duration } = task;
+ if (includeHoliday) return task.duration;
+ let res = 0;
+ const endLen = duration + start;
+ for (let i = start; i < endLen; i++) {
+ if (isHoliday(new Date(taskStartDate + taskDayCount * i)).isHoliday) continue;
+ res++;
+ }
+ return res;
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..9012c1c
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,3 @@
+export { default as TimeLine } from './components/TimeLine';
+export * from './components/TimeLine/interface';
+export * from './components/TimeLine/utils';
diff --git a/src/index.tsx b/src/index.tsx
deleted file mode 100644
index 91bafd0..0000000
--- a/src/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Hello } from './components/Hello';